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

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

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

2078 слоў

Калі час запуску фронтэнду становіцца дужа дзьвялым, перша рэакція — апрацаваць віну на інструмент для зборкі коду і пачаць планаваць перайшчы на іншы. Аднак такая рэакція часта ўскладнена. У цым аналізе рассказваецца пра платформу з мікро-фронтэндам, дзе час запуску за дапамою 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, заўсёды залічваючы свой экасистему.