Дисклеймер: Данный кейс основан на архитектуре нашего проекта (~2600 файлов). В проектах другого масштаба или с другой структурой зависимости результаты могут отличаться. Это не «серебряная пуля», а мой личный опыт оптимизации конкретной инфраструктуры.
Буду краток, на проекте, где я сейчас работаю, мы с командой заметили огромную проблему со скоростью сборки и весом проекта после билда. Стек у нас React, Next.js, FSD Monorepo.
Изначальный набор инструментов был такой:
|
Билд |
Turbopack |
|
Линтинг |
Eslint |
|
Проверка типов |
TypeScript Compiler |
|
Форматирование |
Prettier |
|
Мертвый код |
Knip |
Как видите, вполне стандартный набор инструментов для проекта на React+Next.js, к которым все привыкли и многих они устраивают. Но в какой‑то момент я устал ждать всё то время, пока все проверки пре‑пуш‑хука пройдут. Тем более, что из‑за кривой настройки проверка проходила не на изменённые файлы, а на весь проект: несколько минут ждать только чтобы узнать, что ты не можешь сделать пуш из‑за другого человека, и так по кругу. Так что я сделал полноценную миграцию на нативные инструменты и настроил их.
Начал я с линтера: стандартный Eslint, как оказалось, даже не был способен дойти до конца проекта на базовых настройках. Он просто падал в OOM(Out‑of‑Memory), но даже в таких условиях линтинг этой малой части занимал 35.2с на не полную проверку проекта. После увеличения лимитов оперативной памяти до 8 гигабайт, проверка всего проекта наконец‑то смогла пройти, но заняла 146.3с, то есть, на каждый пуш приходилось ждать 2.5 минуты просто на один этап из двух.
Заменой ему стала связка из oxlint и ast‑grep, поскольку один лишь oxlint не поддерживал все правила линтинга, что нам были необходимы. Такая, казалось бы, минорная и простая замена на два нативных инструмента позволила срезать время линтинга с 146.3с до жалких 14.5с. Таким образом, каждый пуш стал на 2 минуты короче для каждого члена команды, что даже при 5 пушах от разработчика в день сэкономило 20 минут времени каждому и часы, если считать общее время всех разработчиков. Дополнительно срезалось потребление оперативной памяти с 6.91GB на Eslint до 2.79GB на oxlint+ast‑grep.
|
|
Eslint |
oxlint+ast‑grep |
Разница |
|
Время |
146.3с |
14.5с |
10.08х |
|
Потребление RAM |
6.91GB |
2.79GB |
2.47х |
Следующим на разделочный стол попал tsc. Он был просто медленным, проходил весь проект за 101.9 с, что тоже достаточно долго, но хотя бы без OOM. Его я заменил на tsgo, по памяти он остался почти таким же, из‑за специфики проверки типов в коде, но вот время упало с 101.9с на tsc до 9с на tsgo, при той же стабильности.
|
tsc |
tsgo |
Разница |
|
|
Время |
101.9с |
9с |
11.3х |
|
Потребление RAM |
1.55GB |
1.54GB |
1х |
А вот дальше пошел в ход самый неоднозначный результат в этом замере: билд. В базе, как я писал ранее, у нас был Turbo с его 8.9MB после всех процедур(JS+css) и скоростью билда в 18.6с. Это очень быстро, этим результатом он даже обогнал базовый Webpack+Terser в 3 прохода с его 9.2MB и билдом в 147.5с, что в 8 раз дольше чем Turbo так еще и проигрывает по весу. Но у Webpack есть огромное преимущество — контроль над ним. Мы можем сами контролировать, как он будет резать и собирать чанки. После пары часов настроек я смог добиться общего веса проекта в 5.8MB, что уже почти пик, но ещё не полный.
Дальше в ход пошли уже относительно нестандартные техники вроде:
-
стабы next,
-
пре билд и пост билд скриптов,
-
обрезки ядра React,
-
сжатие css после билда.
Дали они очень интересный результат: общий вес они порезали относительно мало (660кб), если сравнить с корректной настройкой самого билдера, но вот максимальный вес чанка они смогли очень сильно срезать, более чем в 2 раза.
Важно: эти манипуляции требуют глубокого понимания графа зависимостей проекта и могут привести к регрессиям, если не покрыты тестами. Также отмечу, что я использовал стабы не для уменьшения общего веса проекта, а для уменьшения веса edger чанков, что в разы критичнее, чем общий вес, так что даже срез 50кб raw веса с главной стоит в разы больше в моем случае, чем 200кб суммарного веса проекта.
Пределом же оказался Rspack, который позволил достичь времени билда в 61.2с и при этом веса в 5.78MB. Я назвал этот инструмент самым неоднозначным по одной простой причине: в базе, без настроек он проигрывает в весе всем и очень сильно, как и видно из бенчмарка.
|
Тип сборки |
Время сборки |
потребление RAM |
Суммарный вес |
|
rspack база |
69.0с |
1.53 GB |
13 881 KB |
|
webpack база |
147.5с |
2.30 GB |
9 379 KB |
|
turbopack |
18.6с |
2.56 GB |
9 125 KB |
|
rspack настройка |
61.2с |
3.46 GB |
5 780 KB |
|
webpack настройка |
136.6с |
3.83 GB |
5 798 KB |
|
rspack трюки |
104.0с |
1.66 GB |
5 088 KB |
|
webpack трюки |
106.9с |
2.28 GB |
5 145 KB |
Но его пик оказался на нашем проекте лучшим, за счет гибкой настройки самого Rspack удалось достичь аномальных результатов в весе огромного проекта. Еще хочу сказать кое‑что интересное про сам turbo: у него есть огромная проблема, а именно то, как он режет чанки. Если сейчас зайти на наш проект или же на ремангу, после чего открыть вкладку «сеть», можно увидеть крайне неприятную картинку: количество запросов js чанков+css+html стремится к сотне, а каждый запрос — это дополнительный оверхэд на установку, tcp‑соединение и так далее.
Вопрос сетей и сетевых задержек от такого количества чанков — это отдельная огромная тема, которую стоит рассматривать в отдельной статье. Пока что просто зафиксируем, что избыточное количество запросов приводит к плохому результату и долгой загрузке , статья и так получилась длиннее, чем я ожидал.
Теперь про Prettier. Сам по себе инструмент не так плох, со своей работой он справляется, но у меня есть личная неприязнь к инструментам на Js, так что я решил заменить его тоже. Замена нашлась достаточно быстро — Biome, тут расписывать в принципе нечего, просто форматер кода, в моем случае. Хотя в себе он имеет также: линтер, сортировку импортов, поиск по коду, авто миграцию конфига, lsp‑proxy и hot daemon. Использовать это всё не стал, так‑как настроил на тот момент уже oxlint. Вот сама таблица времени прогонов на всем репозитории.
|
|
Холодный прогон |
Горячий прогон |
RAM |
|
Biome |
3.7с |
1.5с |
162MB |
|
Prettier |
41.1с |
34.7с |
399MB |
Как видно из прогона, выигрыш огромен, особенно в горячем прогоне, там он в 24 раза.
Также расскажу про полную замену всеми любимого Knip. Он получился тоже достаточно интересным, тестировал я его и его замену Fallow на дефолтных конфигах и на реальных продакшен конфигах. Получилась интересная ситуация. Fallow быстрее, что логично, но разница уже небольшая — всего 2.7 раза на холодном прогоне, 4.7с у fallow против 12.7c у Knip, и 6 раз на горячем прогоне — 1.7с у Fallow против 10.2c у Knip. Насчет работы обоих инструментов сложно что‑либо сказать, поскольку оба очень сильно зависят от настройки, но на базовых настройках Fallow нашел на 19% больше файлов, но это абсолютно не показатель.
|
Холодный прогон |
Горячий прогон |
RAM |
|
|
Fallow |
4.7с |
1.7с |
211MB |
|
Knip |
12.5с |
10.2с |
531MB |
Тут, как видно, выигрыш в скорости скорее минорен, особенно учитывая, что инструмент используется относительно редко, но всё же выигрыш есть.
Также, думаю, стоит затронуть достаточно базовую, в моём понимании, тему: различие между npm и pnpm, и в чем pnpm превосходит уже ставший стандартом npm.
Для начала стоит разобраться, как вообще работают обе команды.
Начнем с npm, он ведет себя максимально просто: открывает ваш package.json и качает все зависимости и иногда зависимости зависимостей прямо в ваш проект в папку node_modules.
И естестественно, у такого подхода есть проблемы, начнем с самых очевидных:
Скачивание, при каждом npm install вы копируете его из кэша, что достаточно долгая и тяжелая операция.
Из этого вытекает вторая очевидная проблема, это вес: если работать с одним проектом, то проблем почти нет, но как только вы начинаете работать с двумя или более проектами, вес становится заметным, особенно, если стек похож, поскольку npm заботливо скачивает вам для каждого проекта свои зависимости.
Далее идет не очень очевидная проблема: в случае конфликта версий, npm прямо в папке node_modules создает еще одну папку node_modules и скидывает уже в нее файл с конфликтом версий. И тут происходит страшное: если у этой зависимости есть свои транзитивные зависимости, то их тоже качает и начинает раздувать вес node_modules еще больше.
Ну а теперь самое страшное: это фантомные зависимости. Вы скачали, например, библиотеку, которой нужен lodash, и она появилась в вашем node_modules, после чего вы или другой разработчик в любом файле пишет import lodash from 'lodash'; а за счет того, что npm имеет плоскую структуру и каждая зависимость пытается встать в корневой node_modules, у него всё запускается и работает, при том, что в package.json lodash не указан. Казалось бы, не указан и не указан, что такого, но тут та библиотека, которую вы поставили, становится не нужна или обновляется, и ей становится не нужен lodash → следовательно, этот самый lodash удаляется из node_modules, и билд начинает падать из‑за ошибки импорта.
Это были все основные проблемы npm, теперь — как эти проблемы решает pnpm.
Проблема 1 и 2 решается одновременно: при скачивании библиотеки через pnpm install, она ставится не в node_modules, а в специальную папку (в базе ~/.local/share/pnpm/*), и прокидывает в вашу папку node_modules хард линк. Когда вы пишете pnpm install повторно, сначала идет проверка, есть ли у вас уже эта зависимость в базовой папке. Если есть, то pnpm вместо долгого и тяжёлого копирования просто прокидывает дешевый хард линк и в другую папку node_modules в другом проекте. Таким образом, скачав один раз зависимость, вы получаете ее бесплатное использование во всех проектах.
Проблема 3 и 4 решается за счет симлинка: в pnpm зависимость не пытается вылезти в корневой node_modules, и ситуация, когда две зависимости конфликтуют по версиям, почти невозможна. А фантомные зависимости в принципе становятся невозможны.
Какой итог я для себя вывел: индустрия нативных инструментов для проектов уже достаточно зрелая, чтобы использовать ее в реальном продакшене и получать с этого реальный выигрыш в скорости. Ваше мнение, естественно, может отличаться в зависимости от вашего опыта, я всегда готов к обсуждение в комментариях.
Вот сам бенчмарк, кому интересно можете использовать и протестировать на своем next.js monorepo https://github.com/BezSaharaD/Benchmark
Все замеры были произведены на Ryzen 5 3600 и 16GB оперативной памяти 3733MHz с таймингами 16–18-18-36-86


ссылка на оригинал статьи https://habr.com/ru/articles/1055360/