- PVSM.RU - https://www.pvsm.ru -
Появляется все больше SPA
салонов. Даже лендинги люди пилят на React. А действительно сложное веб-приложение уже трудно представить с другим подходом. Одна из главных проблем современного фронтенда — это сборка таких проектов. С этим помогают справляться бандлеры.Иван Соснин, фронтенд-разработчик Контура, рассказывает как настроить webpack 2 и 3, чтобы получить ощутимый прирост в скорости сборки статики. Статья будет полезна тем, кто уже работает с webpack или смотрит в его сторону.
Стоит начать с ремарки, что недавно вышел webpack 4 [1]. Там вообще все супербыстро и ничего делать не надо, а еще изменилось процесс разбиения кода на чанки [2].
Но тащить в продакшен библиотеки, которые обновились вчера — не мой путь.
Webpack [3] — это сборщик модулей (бандлер). Он собирает различные модули с зависимостями в один или несколько файлов (бандлов). У webpack модульная архитектура, а это значит, что его можно гибко настраивать. Сборка кода настраивается при помощи плагинов [4], а трансформации кода производятся с помощью загрузчиков (loaders) [5].
Если хочется больше базовых подробностей, можно почитать статью [6] Рахима Давлеткалиева про webpack 1. Она немного устаревшая, но идеи и примеры в ней разобраны подробно.
За всю эту гибкость приходится платить сложной конфигурацией.
Настройка webpack ранних версий — был процесс творческий и мог длиться бесконечно. Ситуация несколько изменилась с выходом второй версии и появлением внятной документации [7]. Но остается много настроек, которые не лежат на поверхности. Это связано с тем, что существует много open-source решений, которые встраиваются в процесс сборки.
Другие сборщики:
require()
для браузера. По возможностям сильно уступает webpack (умеет работать только с JS);У нас в проекте довольно много клиентского кода: ~2000 js/jsx файлов (~300000 строк) и ~800 файлов scss (~50000 строк). Всю эту красоту нужно как-то собирать и для этого мы используем webpack 3. Очевидно, что с ростом кодовой базы скорость сборки выше не станет. Это значит, что нужно искать пути оптимизации скорости сборки. Вообще, на эту тему уже есть довольно [13] много [14] статей [15] и обсуждений [16], но они обычно затрагивают какую-то одну часть сборки (кеширование, пребилд вендорных библиотек и т.д.). Я собрал различные направления оптимизации с конкретными примерами.
Для разных проектов были разные результаты. Например, в одном соседнем проекте скорость сборки выросла с 3.5 минут до 30 секунд. Для моего проекта статистика ниже.
До всех изменений | После изменений | |
---|---|---|
"Холодный" билд для продакшена | 14 минут | 3 минуты |
Ребилд для продакшена | 12 минут | 2 минуты |
"Холодный" билд для разработки | 17 минут | 3 минуты |
Ребилд для разработки | 5 минут | 30 секунд |
Процесс билда во всех случаях одинаковый: сначала нужно установить зависимости, затем собрать 4 приложения, в которых по несколько entry points.
В данном случае "холодный билд" подразумевает, что очищены все кеши проекта (кроме локального кеша yarn, об этом далее), нет папки node_modules. А ребилд подразумевает повторный запуск сборки.
Я разделил билды для продакшена и для разработки, потому что у них отличаются конфиги, например, в билде для разработки совсем нет Uglify.
Дальше я покажу, какой пункт какого именно изменения в скорости позволил достичь. Цифры вполне могут быть непоследовательны, т.к. в разные моменты замера могла быть разная конфигурация вебпака. Стоит больше обращать внимания на порядок размера изменений.
Я использую yarn [17]. Он довольно удачно зарелизился и решал многие проблемы нативного клиента npm на тот момент.
В октябре 2016 [18], когда вышел yarn, npm был версии 3.10.9 [19] и до релиза версии 5.0.0 [20] было еще примерно полгода. Некоторые проблемы, которые решил yarn:
- механизм фиксации всех зависимостей был костыльный: была лишь команда
npm shrinkwrap
, которая создавала lockfile;- npm был сильно зависим от стабильности сети. Эту проблему решала тулза shrinkpack [21], которая архивировала все текущие зависимости и подменяла пути в lockfile на локальные. Все бы ничего, но все эти тысячи архивов нужно было таскать в репозитории. И при любом мерже ловить конфликты в бинарях. Подробнее про эту тему можно узнать из интересного доклада с WSD 2016 в Екатеринбурге [22];
- повторная установка пакетов, если ничего не менялось, все равно длилась какое-то время;
- субъективно, но меня сильно порадовал визуальный режим обновления пакетов:
yarn upgrade-interactive
, хотя и для npm есть аналоги [23].
Сейчас уже есть альтернативы yarn: например, npm научился кешировать, и еще есть pnpm [24], который вообще в node_modules только хардлинки создает.
Сравнение скорости установки на ~1300 пакетах:
npm 5 | yarn 0.24.6 | pnpm | |
---|---|---|---|
Установка с локальным кешем | ~1 минута | ~3 минуты | ~1 минута |
Повторная установка | ~20 секунд | ~1 секунда | ~1 секунда |
Lockfile |
Можно почитать еще занятное сравнение на hackernoon [25].
И если вы до сих пор не фиксируете зависимости в package.json — самое время начать.
Конечно, в том случае, если вы трансформируете код и используете babel [26]. Webpack-конфиг будет примерно такой:
test: /.jsx?$/,
use: [{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}]
По умолчанию кеш складывается в node_modules/.cache/babel-loader, но можно указать другой каталог.
Разница: 667 сек ⟶ 614 сек (8%)
Плагин для webpack [27], который кеширует собранные модули. Есть большой issue [28] на тему кеширования в webpack, там и зародилась идея этого плагина. По ссылке как раз пост автора плагина.
Подключается в конфиге webpack:
plugins: [
new HardSourceWebpackPlugin()
]
По умолчанию кеш складывается в node_modules/.cache/hard-source, но можно указать другой каталог.
В моем случае, просто подключение этого плагина без конфига, дало прирост с 200 секунд до 50 (при наличии кеша).
При использовании webpack-dev-server и postcss придется поработать напильником [29].
Замеченные проблемы:
Разница: 275 сек ⟶ 53 сек (80%)
UglifyJS [30] — это инструмент, который используется для минификации JS-кода. Webpack добавляет его в плагины автоматически, если собирать бандл с флагом -p [31].
Для использования webpack-parallel-uglify-plugin [32] есть 2 причины:
Если использовать этот плагин, то придется вручную добавлять его в продакшен-сборку и уже не пользоваться флагом -p. Пример конфига:
plugins: [
new ParallelUglifyPlugin({
cacheDir: path.join(dir.root, "node_modules", ".cache", "parallel-uglify"),
uglifyJS: {/* uglifyjs options */}
})
]
Разница: 627 сек ⟶ 391 сек (38%)
Этот пункт следует из описанных выше. Не стоит удалять node_modules перед деплоем клиентского кода.
У многих наверняка подключены в проектах библиотеки или фреймворки, которые используются по всему проекту. Такие библиотеки можно каждый раз не пересобирать. С этим нам поможет пара встроенных в webpack плагинов: DllPlugin и DllReferencePlugin [33].
Для начала нужно вынести в отдельный конфиг сборку DLL. Это ваш обычный webpack-конфиг, где обязательно должен быть подключен DllPlugin:
// webpack.vendor-dll.config.js
new webpack.DllPlugin({
name: 'vendor',
path: 'prebuild/' + environment + '/vendor-manifest.json',
})
Переменная environment здесь — это process.env.NODE_ENV. Потому что я хочу разделять девелоперскую и продакшн сборки DLL.
Для установки process.env.NODE_ENV можно посмотреть на пакет cross-env [34]. Тогда npm-script может выглядеть как-то так:
"deploy:app1": "cross-env NODE_ENV=production webpack --progress --config ./path/to/app1/webpack.config.js",
После сборки у вас получится 2 файла: vendor-manifest.json и какой-нибудь dll.vendor.js. Их нужно закоммитить в репозиторий. По крайней мере, версии для продакшена.
В вашем основном конфиге нужно добавить DllReferencePlugin:
// webpack.config.js
new webpack.DllReferencePlugin({
manifest: require('./prebuild/' + NODE_ENV + '/vendor-manifest.json'),
})
Возможно, вы хотите, чтобы DLL, который вы коммитите в репозиторий, лежал рядом с вашими бандлами. Здесь вам поможет CopyWebpackPlugin [35]:
new CopyWebpackPlugin([
{
context: path.join(__dirname, 'prebuild', NODE_ENV),
from: '*',
},
], {
ignore: [
'webpack-vendor-assets.json',
'vendor-manifest.json',
],
})
Разница: 233 сек ⟶ 213 сек (9%)
Начиная с версии 0.15 css-loader начал сильно замедлять сборку [38]. Судя по комментариям, у некоторых [39] сборка замедлилась больше, чем в 50 раз. В моем же случае разница была, но не такая большая.
Список фич, которые нельзя будет использовать при понижении версии:
Но CSS Modules и scope вполне можно использовать. Полная документация для версии 0.14.5 [46].
Разница: 213 сек ⟶ 185 сек (13%)
Этот webpack-плагин [47] умеет выносить общий код указанных модулей в отдельный чанк. То есть, если у вас есть 2 бандла 1.bundle.js и 2.bundle.js, и в обоих используются, скажем, React и Redux, они окажутся в отдельном чанке, а в бандлах их не будет.
Пример и результаты использования можно посмотреть в репозитории webpack [48]. А более подробно работа плагина описана в теме на StackOverflow [49].
Пример конфига:
plugins: [
new webpack.optimize.CommonsChunkPlugin({
names: ["common", "manifest"],
minChunks: Infinity
})
]
При этом у меня один entry point (не минифицированный) похудел с 4.4 Мб до 4.2 Мб (5%).
Разница: меньше 10 сек
Экспериментируйте [50]! И изучайте ваши бандлы [51]! И снова экспериментируйте!
Эта настройка webpack [52] позволяет избежать парсинга определенных библиотек или файлов. Webpack просто добавит такой модуль в бандл, без преобразования.
То есть, если какая-то библиотека доставляется в npm в минифицированном виде и в ней нет никаких require и, например, кода, который нужно компилировать, ее можно не прогонять через webpack, потому что это будет бесполезно и может быть долго, а сразу засунуть в бандл.
Разница: меньше 10 сек
Эта настройка webpack [53] позволяет кешировать модули и чанки. Включена по умолчанию в режиме --watch
.
Разница: меньше 10 сек
Этот плагин [54] не столько про скорость сборки, сколько про размер бандла. Некоторые библиотеки [55] при подключении тянут за собой тонну мусора (например, локализации для кучи языков). При помощи ContextReplacementPlugin можно это исправить:
plugins: [
new webpack.ContextReplacementPlugin(/moment[/\]locale$/, /ru/)
]
При этом у меня один entry point (не минифицированный) похудел с 4.2 Мб до 3.8 Мб (10%). CommonsChunkPlugin был отключен.
Разница: меньше 10 сек
До этого момента мы различными способами ускоряли сборку отдельных entry-points. Но если у вас их много, или много разных webpack-конфигов, их можно собирать параллельно. Есть замечательная обертка [56], которой можно передать массив webpack-конфигов и они будут собраны параллельно.
У меня получился примерно такой конфиг:
const app1Config = require("./App1/webpack.config");
const app2Config = require("./App2/webpack.config");
const app3Config = require("./App3/webpack.config");
const app4Config = require("./App4/webpack.config");
module.exports = [
app1Config,
app2Config,
app3Config,
app4Config
];
В итоге сборка занимает столько времени, сколько занимает сборка самого жирного проекта.
Замеченные проблемы:
Разница: 178 сек ⟶ 119 сек (33%)
Это такой пакет [58], который запускает все трансформации кода параллельно.
Чтобы его настроить, нужно перетащить настройки для основного лоадера в настройки плагина HappyPack:
const HappyPack = require("happypack");
// ...
plugins: [
new HappyPack({
loaders: ["babel-loader"]
})
]
А вместо них добавить happypack loader:
module: {
rules: [
{
test: /.jsx?$/,
use: "happypack/loader"
}
]
}
У себя на Windows я его завел, но ощутимого прироста в скорости не получил, так что HappyPack я не использую. Судя по всему, я такой не один: issue [59], issue [60].
Разница на Windows: меньше 10 сек
Автор: ylian_demakova
Источник [61]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/275196
Ссылки в тексте:
[1] вышел webpack 4: https://medium.com/webpack/webpack-4-released-today-6cdb994702d4
[2] разбиения кода на чанки: https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
[3] Webpack: https://webpack.js.org
[4] плагинов: https://webpack.js.org/concepts/plugins/
[5] загрузчиков (loaders): https://webpack.js.org/concepts/loaders/
[6] статью: https://habrahabr.ru/post/309306/
[7] внятной документации: https://webpack.js.org/configuration/
[8] browserify: https://github.com/browserify/browserify
[9] rollup: https://github.com/rollup/rollup
[10] parcel: https://github.com/parcel-bundler/parcel
[11] до недавних пор: https://github.com/parcel-bundler/parcel/issues/68
[12] сделал шаг: https://medium.com/webpack/webpack-4-beta-try-it-today-6b1d27d7d7e2#b8ad
[13] довольно: https://medium.com/ottofellercom/0-100-in-two-seconds-speed-up-webpack-465de691ed4a
[14] много: https://github.com/webpack/docs/wiki/build-performance
[15] статей: http://engineering.invisionapp.com/post/optimizing-webpack/
[16] обсуждений: https://github.com/webpack/webpack/issues/1574
[17] yarn: https://github.com/yarnpkg/yarn
[18] октябре 2016: https://yarnpkg.com/blog/2016/10/11/introducing-yarn/
[19] 3.10.9: https://github.com/npm/npm/releases/tag/v3.10.9
[20] 5.0.0: https://github.com/npm/npm/releases/tag/v5.0.0
[21] shrinkpack: https://github.com/JamieMason/shrinkpack
[22] интересного доклада с WSD 2016 в Екатеринбурге: https://youtu.be/FlxpXoiiiT4?t=5h18m52s
[23] есть аналоги: https://github.com/th0r/npm-upgrade
[24] pnpm: https://pnpm.js.org/
[25] сравнение на hackernoon: https://hackernoon.com/understanding-differences-between-npm-yarn-and-pnpm-31bb6b0c87b3
[26] babel: https://github.com/babel/babel-loader#options
[27] Плагин для webpack: https://github.com/mzgoddard/hard-source-webpack-plugin
[28] Есть большой issue: https://github.com/webpack/webpack/issues/250#issuecomment-240643985
[29] поработать напильником: https://github.com/mzgoddard/hard-source-webpack-plugin#troubleshooting
[30] UglifyJS: https://github.com/mishoo/UglifyJS2
[31] флагом -p: https://webpack.js.org/api/cli/#shortcuts
[32] webpack-parallel-uglify-plugin: https://github.com/gdborton/webpack-parallel-uglify-plugin
[33] DllPlugin и DllReferencePlugin: https://webpack.js.org/plugins/dll-plugin/
[34] cross-env: https://github.com/kentcdodds/cross-env
[35] CopyWebpackPlugin: https://github.com/kevlened/copy-webpack-plugin
[36] пример в репозитории webpack: https://github.com/webpack/webpack/tree/d6d6134ca634e1cc02d76be74341a43ff994dc7e/examples/dll-app-and-vendor
[37] пример, который я приготовил: https://github.com/vansosnin/webpack-dll-example
[38] начал сильно замедлять сборку: https://github.com/webpack-contrib/css-loader/issues/124
[39] некоторых: https://github.com/webpack-contrib/css-loader/issues/124#issuecomment-232270044
[40] Композиция: https://github.com/webpack-contrib/css-loader#composing
[41] наследоваться: https://github.com/webpack-contrib/css-loader/tree/51e11f3588c8bde66c5cd6b6d7b9bbbdeda671c4#inheriting
[42] camelCase: https://github.com/webpack-contrib/css-loader#camelcase
[43] URL disable: https://github.com/webpack-contrib/css-loader#url
[44] Alias: https://github.com/webpack-contrib/css-loader#alias
[45] @import disable: https://github.com/webpack-contrib/css-loader#import
[46] Полная документация для версии 0.14.5: https://github.com/webpack-contrib/css-loader/tree/51e11f3588c8bde66c5cd6b6d7b9bbbdeda671c4
[47] Этот webpack-плагин: https://webpack.js.org/plugins/commons-chunk-plugin/
[48] в репозитории webpack: https://github.com/webpack/webpack/tree/master/examples/multiple-commons-chunks
[49] в теме на StackOverflow: https://stackoverflow.com/questions/39548175/can-someone-explain-webpacks-commonschunkplugin
[50] Экспериментируйте: https://twitter.com/TheLarkInn/status/842817690951733248
[51] изучайте ваши бандлы: https://github.com/webpack-contrib/webpack-bundle-analyzer
[52] Эта настройка webpack: https://webpack.js.org/configuration/module/#module-noparse
[53] Эта настройка webpack: https://webpack.js.org/configuration/other-options/#cache
[54] Этот плагин: https://webpack.js.org/plugins/context-replacement-plugin/
[55] Некоторые библиотеки: https://momentjs.com/
[56] замечательная обертка: https://github.com/trivago/parallel-webpack
[57] иногда зависает: https://github.com/trivago/parallel-webpack/issues/41
[58] Это такой пакет: https://github.com/amireh/happypack
[59] issue: https://github.com/amireh/happypack/issues/70
[60] issue: https://github.com/amireh/happypack/issues/188
[61] Источник: https://habrahabr.ru/post/351080/?utm_campaign=351080
Нажмите здесь для печати.