Галоўная / Артыкулы

Аналіз прычын спадзеючага часу запуску Webpack Monorepo пры прабоўце выкарыстаць новы інструмент для збірання коду

Як аналіз процеса складання мікро-фронтэнда на Webpack працэю, яка трывала 20 хвілін, паказаў на викорыстанне Terser, Babel, процесы інсталяцій і кэшавання ў Docker, а таксама якія змены дапамоглі скорачыць час складання да апошніх двух хвілін.

Выдавец: REACTAPP.TOP9 хв чытання2078 слоў

Крыніцы: Build at Zepto Speed: How We Cut Frontend Webpack Build Times by 95%

Калі час запуску фронтэнду становіцца дужа дзьвялым, перша рэакція — апрацаваць віну на інструмент для зборкі коду і пачаць планаваць перайшчы на іншы. Аднак такая рэакція часта ўскладнена. У цым аналізе рассказваецца пра платформу з мікро-фронтэндам, дзе час запуску за дапамою Webpack перавышаў 20 хвілін; аналіз показаў, што сам інструмент не быў прычыной проблемы, і што серыя ціляваных змян дазволіла скорачыць час запуску да праўяко 2 хвілін, не заменяючы Webpack. Вы пазнаеце, як вывучыць справжню прычыну затрымкі, якія інструменты на базе Rust заменілі певныя етапы процесу, і чаму установка залежнасцей, токены реестру і шары Docker мелі такое ж значэнне, як і будь-які іншы інструмент для зборкі.

Калі час запуску становіцца проблемай платформы

Указаная платформа ў месяц прымала аддаўна 600 запросаў пра змяны ад падаць дзесяці інжынераў, што прыводзіла да аддаўна 5 000 запускаў процедуры CI. Працаваючы 20 хвілін на кожны запуск, це станавіла аддаўна 1 600 гадзін часу CI ў месяц. Пры такой колькасі запускаў повалка процедуры стае не проста дратуючай проблемай, а бутылочным горлам для всей організацыі: адпаведныя звёсткі прыходзяць пазнела, чергі запускаў CI растуць, а трыбутныя практычныя правкі чакаюць у черзі.

Структура монорепа толькі пагаршала ситуацыю:

20 production apps: React and Next.js applications
7 shared libraries
1 centralized E2E test suite
~3,950 TSX/JSX files and 6,600+ TypeScript source files
~350 reusable component modules
144 npm dependencies (74 production, 70 development)
Workspace: single hoisted monorepo
Team: 10+ engineers, 600+ PRs/month
CI: 5,000+ build runs/month

Двадцать прыкладнікаў, якія дзелюць семь бібліятэкаў у аднай працоўнай суперактуальнасі, значыць, кожная змяна у інструментах паўтараецца для всіх адразу.

Чаму спачатку была дыягностика, а потым міграцыя

Замена інструмента для складання кода ў 20 прыкладных дапрацоўках, 7 спільных бібліятэках і 144 залежнасцях — гэта проект з высокім рызыкам. Блакберг у адной з спільных бібліятэкаў можа пашкодзіць кожную дапрацоўку, а перапраналізаванне всіх рэзультатаў складання можа займаць тыяжкія тыдні без гарантій успеху. Таму команда спачатку аналізавала весь процес. У ходзе гэтага аналізу былі выявлены значныя витраты, не relacionаваныя з самым інструментам для складання кода, у такіх аспектах, як перакантрольванне типаў, організаванне тэстаў і рашэнне залежнасцяў, пры чым ні новы інструмент для складання кода, ні якія-іншы заходы не маглі гэта паспрабаваць пакращыць.

Шырэйшы урок знайомы з любай роботы па падвышэнню карыстнасці: оптымізацыя перад мераваннем прыводзіць да таго, што кожная змяна стае простаю спадчынай. Процес складання з Webpack праходзіць через калькі разлічных фаз, перш чым ствараецца готовы продукт:

  • кампіляванне модуляў
  • экспанацыя ладераў
  • стварэнне чакаў
  • оптымізацыя ресурсаў
  • мініфікацыя
  • выдача ресурсаў

Без часовых показань для кожнага этапу немагчыма з’ясаваць, які з іх патрабуе ўвагі, таму нічога ў налаштаваннях ладера чы плагіна не было зменяна, пакуль не былі вказаны ці фігуры.

Пошук актуальнага месца з ProgressPlugin

Webpack поставляецца з ProgressPlugin, які не патрабуе додатковага інсталлявання. У файле налаштавань неабходна ўстановка Webpack:

const webpack = require('webpack');

і трэба дадаць плагін у масэвы plugins:

plugins: [
  new webpack.ProgressPlugin()
]

Калі быў увёрнуты выхід дадзеных пра профілюванне, адразу ж выдзелілася адна лінія:

[webpack] 92% sealing > asset processing
TerserPlugin took 825.31s

Адзіны плагін займаў больш чым 13 хвілін. Порашчы з іншымі плагінамі на стадії оптымацыі, гэта неравенства ўсё бол выражаныя:

copy-webpack-plugin      30ms
WriteIndexHtmlPlugin     22ms
RealContentHashPlugin    58ms
CompressionPlugin        70ms
LicenseWebpackPlugin     1.15s
TerserPlugin             825.31s

Кожны другі плагін завершаў свою роботу за каліксекунды, а плагін з ліцензіям — прыбліжна за секунду. Сам збірник коду не быў повольным; проблемай была мініфікацыя JavaScript за дапамогою Terser. Гэтыя спостерэння змусілі зменіць падход да роботы.

Время виконання на рэвеніі ладера з плагіном Speed Measure

ProgressPlugin паказвае, яка стадія є повольной, але не вказывае, яка ланцюг ладера пад час компіляцыі ёсць прычыной. Для гэтага команда дадала speed-measure-webpack-plugin, які обгортае экспортаваную настройку:

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap(config);

SMP адаптава інфармацыю пра тое, сколькі часу кожны лоадер выкарыстоўвае на обробку модуляў, што дазволіла парабяліць процес роботы прыемнікай да і пасля введэння SWC. Ён таксама даваў чыстую рэальную інфармацыю. Ланцоўка CSS, якая складаецца з mini-css-extract-plugin, css-loader і postcss-loader, не стала быстрэйшай; час ў яе вырас з 8,66 да 9,36 секунд. CSS і модулі, якія не обрабляюцца жадным лоадерам, займалі майже 16 з застаўшыхся 20,78 секунд загальнага часу выходу. Узамест канфліктавання па праблеме выбору інструментоў, команда мела конкрэтныя цыфры, якія паказвалі, дзе трэба здзейсніць наступныя змены.

Є адна застерэжніца, яку варта знати: SMP абгортвае плагіны і лоадеры, каб змерыць час их роботы, і вядома, што ён можа стварыць канфлікты з дзеянымі новейшымі плагінамі, таму яго трэба викорыстоўваць толькі для діагностики, а не залишаць у конфігурацыі для працы.

Цялі, якія абмежвалі кожнае рашэнне

Перад тым, як чаго-небудзь зменіць, каманда запісала цялевыя пункты ў троха груп.

Эфективнасць стварэння

  • Зменшыць час чакання пад час стварэння праекту.
  • Прывычнае стварэння праекта.
  • Менш часу на транспіляцыю і мініфікацыю.
  • Кращая аддача ад наяўных ядроў CPU.

Эфектывасць CI

  • Частэйшая пераўтамовка шароў кешу Docker.
  • Корачэйшая установка залежнасцей.
  • Больша надзеямасць у процесах обробкі.

Стабільнасць платформы

  • Зберагчы стабільнасць інструментаў у масштабе падпрыемства.
  • Ухіліцца ад рызыкаваных міграцый.
  • Зберагчы сумеснасць з існуючай экасистемай.
  • Спраўдзіць первісную швальнасць проты дагоўнае падтрымкі.

Чаму Vite і Rspack былі адкладзены

Vite быў серйозна адначытан. Це высокаяя якосцю програма, якая часта ўтварае правы выбор для новых або простэйшых проектаў. Ключовая разліка закладвалася у тым, што яго працэздатнасць адначылі з рэальным практычным варыянтом, а не з маленькімі дэмантамі, якія ўжо є у багатых вядарах па міграцыі. Вывад быў такім, што галоўныя витраты, звязаныя з скорачэнням розмеру коду, не залежаць ад інструменту для збірання коду: пераход на Vite займе апошней чатыры тыдзены, і каманда будзе сталкавацца з падобными прыгнічэннямі, а таксама з неабходнасцю рашыць проблемы, звязаныя з новым форматам налашчэння і несумеснасцю плагінам. У журнале рашэнняў Vite быў зазначаны як варыянт, які трэба будзе пераглядзець, якщо складанне коду калі-небудзь стане галоўным прыгнічэнням.

Справедліва нюанс: Vite за замовчэнням мініфікуе JavaScript за дапомогою esbuild, а не Terser, таму міграцыя на практыцы таксама зменіла б інструмент мініфікацыі. Це падкрепляе справжню ідею, а не паслабляе яе. Ключовым факторам была выбор мініфікатора, і Webpack можа выкарыстоўваць той жа шырокі мініфікатор без неабяжной міграцыі.

Rspack — інструмент для зборкі коду на базе Rust з конфігурацыяй, сумесной з Webpack — таксама быў рассмотраны і выглядаў перспективным. У той час няштоўнія інцыдэты з безпекай і ще розвіваючыся екосістэма змусілі команду быць абережнымі, таму ён застаўся у списку на пазнейшую перэцэнку. Перад тым, як робіць власныя выводы, пераканайцеся ў його нынешнім стане.

Перабудова самых повольных этапаў

Калі стратэгія была визначана, робота перайшла да поступовага замены самых повольных компанентоў.

SWC замест Babel для транспіляцыі

Транспіляцыя JavaScript і TypeScript была аднай з найбольшых застаўшыхся витатак. Babel быў заменены на SWC — кампайляр на базе Rust, прызначаны для шыракага спектру трансформацый JS і TS, які включаецца за дапамой swc-loader, а яму прадшычы thread-loader:

{
  test: /\.(jsx?|tsx?)$/,
  use: [
    'thread-loader',
    {
      loader: 'swc-loader'
    }
  ]
}

Публічныя тэсты паказваюць, што SWC транспіляе значна быстрэй за Babel, і ў гэтым ланцугу пераход на SWC прынёс болей шырокую транспіляцыю, можлівасць паралельной роботы за дапамой thread-loader, меньшую блокаванне галоўнага процесу і корачэ выконанне тестав CI. swc-loader у гэтым разе выкарыстоўвае файл .swcrc або вбудованыя настройкі для парсера і JSX; без іх синтаксіс TypeScript і JSX не будзе парсавацца.

esbuild заместо Terser для мініфікацыі

У аналізе вялікія прычыны вядомы былі ўжо раней, таму мініфікацыя JavaScript перайшла на esbuild за дапамой плагіна esbuild-loader:

new EsbuildPlugin({
  target: 'es2015',
  minify: true
})

Рашынак быў адбіты на аснове проекта minification-benchmarks, які парабялеў esbuild, terser, swc, uglify-js і іншыя за розмерам выкаананага коду пасля мініфікавання, розмерам у формате gzip і часам выкаанення. У гэтых дадзенах:

  • Расмер выкаананага коду esbuild пасля мініфікавання і запаковвання ў gzip быў конкурэнтным: на адзін календарны момент ён быў на адзін календарны момент большы за найкращы рэзультат, але залягаў у межах прыблізна 5–8 процэнта ад яго;
  • esbuild выкаанаў задачу за адзін календарны момент 295 мс, тады як terser патрэбаваў адзін календарны момент 6,7 секунды на той самы вхідны дадзенні; гэта расхожанне паўтарылася ў монорепо;
  • @swc/core і oxc-minify стваралі меншы розмер выкаананага коду, але ўслядкі іх медленейшага выкаанення і сложнасці інтеграцыі рашынак быў здабыты на корысть esbuild.

Настройка target: 'es2015' паведамляе esbuild, якую сынтаксіс ён можа выдаваць; пераканайцеся, што яна адпавядае рэальнай падтрымцы браузера, адколькі новейшы цэльны параметр дазволяе ствараць болей коракты код.

LightningCSS для мініфікацыі CSS

Для мініфікаціі CSS выкарыстоўваецца LightningCSS ад команды Parcel, які ўключаецца ў css-minimizer-webpack-plugin як функцыя мініфікацыі:

new CssMinimizerPlugin({
  minify: CssMinimizerPlugin.lightningCssMinify,
})

Бенчмаркі LightningCSS параграфуе яго з інструментамі cssnano та esbuild для мініфікаціі CSS. У кожным пераглядзеным сцэнарыі яго паказуюць як найшырэйшы, з завжды меньшым розмерам выходных файлаў, а таксама з значным падвышэнням карыстоўнасці для вялікіх стайл-шітов, такіх як проекты Tailwind. Для CI-пайплайнаў, дзе оптымізацыя CSS безпасебна вплывае на аб’ём задач, кращая ступень стыснення разам з меншым часам адпрацоўкі зробілі яго явным выборам. Унаследке змены параметраў мініфікаціі, швыдкасць адпрацоўкі JavaScript зросла прыблізна у 10 разоў, а CSS — у 6 разоў.

Іспытанне всіх ядоў CPU

Агенты CI зазвычай маюць калькі ядоў, але багатыя пайплайны залишаюць большасць з іх незанятымі. Адключэнне thread-loader перед дорогімі лоадерамі дазволяе распаўсюдзіць цяперашнюю роботу між разнымі працоўнікамі:

use: ['thread-loader', 'swc-loader']

Это скасавала проблемы затрохкання падчас стварэння проекта, павысіла эфектываўнае выкарыстоўванне рэсурсаў і падняла шырокаспектнасць процеса CI. Памятайце, што кожны працоўнік мае додатковыя витраты на запуск і перадачу паведамленняў; для вяліка праекта, дзе вжо існуе шырокаспектны ладчык у дзеўялік SWC, працоўнікі могу стварыць больш витак, чым заўсёды зарабіць, таму неабходна ацэніць рэзультаты з іх вжыванням і без яго.

Шырэйшыя та болей надзеяныя процесы інсталляціі з pnpm

Компіляцыя не была ўсім виткам; інсталляцыя залежнасцей таксама спрацоўвала хвілінкі часу CI. Команда паўпоручыла npm, yarn, pnpm і bun, выкарыстоўваючы публічныя тэсты, якіе ацэнювалі шырокаспектнасць інсталляціі, выкарыстоўванне дыска і механізмы рашэння проблем. Bun показаў хорашыя результаты у сінтэтычных тэстах, але pnpm выграў за рахунак зрэласці екосыстэмы, сумеснасці з Node.js і дасвядчэнняў у вялікіх прыменных системах.

Калі npm копіюе пакеты ў кожны node_modules, pnpm выкорыстоўвае глобальны хранільнік з адресаванням кантэнту: кожная версія пакета завантажваецца толькі раз і ствараецца жорсткая лінкаванне ў проекты. Це дае кальколька прыямоў:

  • інсталяцыя ўскораная, таму што пакеты запрашваюцца толькі раз і перадаюцца знову;
  • зменшываецца выкарыстоўванне диска, таму што працовыя прасторы больш не дублююць тыя ж пакеты;
  • строгая працэс разв’язкі блакуе фантомныя залежнасці, пакеты, якія ваш код імпортуе без ўказвання іх, што робіць процесы будавання болей дэтерміністычнымі;
  • паспрабоўкі кэшавання шара Docker падбягаюць, таму што хранільнік можна перадаць знову межы процесамі будавання.

У Docker кэш BuildKit дазволяе зберагаць хранільнік pnpm межы процесамі будавання, тады як параметр --frozen-lockfile спрычыняе аббіёкцію інсталяцыі, якщо файл з локамі ўстарэў:

RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
    pnpm install --frozen-lockfile

Дзеянне аутэнтыфікаціі, якое спрычыніла проблему з кэшаванням

Адзін з найэфектывейшых спосабаў адлагоджэння не мяў нічынага з інструментамі фронтэнду. Пакеты прыходзілі з AWS CodeArtifact, чыя токены автарызацыі выгораюць пасля 12 гадзін. Павтаральна запрашэнне токенав у рамках пайплайна спрычыняла анвалідацыю кэшу, павтаральную роботу з автарызацыяй і паспаленне шароў Docker, таму што змяненае значэнне, якое падае ў шар, зміняе ключ кэшу таго шара.

Рашэнням было запрашваць токен аднае раз пры кожнай задачы Jenkins і вяртаць яго для кожнага крока:

export CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token \
  --domain <domain> \
  --domain-owner <owner> \
  --query authorizationToken \
  --output text)

Гэта прынесла лепшую перадаўнае шароў, менш чыстых неабавязковых інсталяцый і стабільнейшыя пайплайны. Якщо вы перадаеце такі токен у процес будовы Docker, лепш выкарыстоўваць секрэтны маунт замест аргумента будовы, каб ён не патрапіў у історыю абраза і не анвалідаваў кэш.

Структураванне Dockerfiles для эфектывае работы кэшу

На завершанне Dockerfiles былі пераканструльваны ў шары, распараджаны ад тых, якія змінююцца рэдка, да тых, якія змінююцца часта:

  1. Базавы час адканалення;
  2. Інсталяцыя залежнасцей;
  3. Копіюванне выхаднага коду;
  4. Складанне пакета за дапамою Webpack.

У поўнай з’еднанасці з параметрамі --mount=type=cache для хранення пакетаў і --mount=type=secret для аутентыкацыйных даных, такія парады значэнняяў абяцвалі, што праз змены ў кодзе будуць пераскладаны толькі два апошнія ўзоракі, чым значна падвышаецца вярнэ іспользоўванне кеша.

Галоўныя выводы

  • Скорасць складання трэба спрыяваць як проблему системы. У даным случае найбольшыя практычныя рэзультаты былі атрыманы за дапамою інструментаў для скарычвання коду, процэсаў інсталляцыі, аутентыкацыйных даных і структурызаціі ўзоракоў у Docker, а не за дапамою самага інструмента для збірання коду.
  • Спачатку аналізуйце ситуацыю. ProgressPlugin знайшоў, што крок скарычвання коду за дапамою Terser займае 13 хвілін; спекулятивная міграцыя зайняла бы недзеўкі.
  • Інструменты на базе Rust, такія як SWC, esbuild і LightningCSS, даўаюць сумарны эфект: кожны з іх усуне частку затрымкі.
  • pnpm і адэкватна кешаванне ў Docker палягаюць на падвышэнні надзеямоўнасці і швальнасці.
  • Будзь-што, што змянюецца ў кожны раз пры запуску, нават токен аутэнтыкацыі, можа таямна знішчыць кеш.
  • Залічвайце міграцыі, але робіце гэта з узглядам на даны. Завяршыўшы модернізацію адзінарых этапаў, гэтая платформа зменіла час запуску з адных 20 хвілін на прыблізна 2, заўсёды залічваючы свой экасистему.