- PVSM.RU - https://www.pvsm.ru -

Ускорение сборки JavaScript-кода с использованием webpack 2–3

Появляется все больше SPA салонов. Даже лендинги люди пилят на React. А действительно сложное веб-приложение уже трудно представить с другим подходом. Одна из главных проблем современного фронтенда — это сборка таких проектов. С этим помогают справляться бандлеры.

Иван Соснин, фронтенд-разработчик Контура, рассказывает как настроить webpack 2 и 3, чтобы получить ощутимый прирост в скорости сборки статики. Статья будет полезна тем, кто уже работает с webpack или смотрит в его сторону.

Стоит начать с ремарки, что недавно вышел webpack 4 [1]. Там вообще все супербыстро и ничего делать не надо, а еще изменилось процесс разбиения кода на чанки [2].

Но тащить в продакшен библиотеки, которые обновились вчера — не мой путь.

Webpack

Webpack [3] — это сборщик модулей (бандлер). Он собирает различные модули с зависимостями в один или несколько файлов (бандлов). У webpack модульная архитектура, а это значит, что его можно гибко настраивать. Сборка кода настраивается при помощи плагинов [4], а трансформации кода производятся с помощью загрузчиков (loaders) [5].

Если хочется больше базовых подробностей, можно почитать статью [6] Рахима Давлеткалиева про webpack 1. Она немного устаревшая, но идеи и примеры в ней разобраны подробно.

За всю эту гибкость приходится платить сложной конфигурацией.

Настройка webpack ранних версий — был процесс творческий и мог длиться бесконечно. Ситуация несколько изменилась с выходом второй версии и появлением внятной документации [7]. Но остается много настроек, которые не лежат на поверхности. Это связано с тем, что существует много open-source решений, которые встраиваются в процесс сборки.

Другие сборщики:

  • browserify [8]require() для браузера. По возможностям сильно уступает webpack (умеет работать только с JS);
  • rollup [9] — позиционируют себя как сборщик, который генерирует самые быстрые и маленькие бандлы;
  • parcel [10] — супербыстрый, но пока зеленый бандлер, не требующий конфигурации. Любопытный зверек, но пока применим только на небольших проектах (до недавних пор [11] вообще не умел в source map). Кстати, и webpack уже сделал шаг [12] к zero configuration.

Немного цифр

У нас в проекте довольно много клиентского кода: ~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.

Дальше я покажу, какой пункт какого именно изменения в скорости позволил достичь. Цифры вполне могут быть непоследовательны, т.к. в разные моменты замера могла быть разная конфигурация вебпака. Стоит больше обращать внимания на порядок размера изменений.

Что можно оптимизировать?

  • установка зависимостей;
  • процесс сборки проекта;
  • инкрементальный ребилд (для разработки);
  • уменьшение количества кода, который попадает в бандл (tree shaking);
  • кеширование на разных уровнях;
  • распараллеливание отдельных этапов сборки.

Используйте npm-клиент, который умеет в кеширование

Я использую 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-loader

Конечно, в том случае, если вы трансформируете код и используете babel [26]. Webpack-конфиг будет примерно такой:

test: /.jsx?$/,
use: [{
    loader: 'babel-loader',
    options: {
        cacheDirectory: true
    }
}]

По умолчанию кеш складывается в node_modules/.cache/babel-loader, но можно указать другой каталог.

Разница: 667 сек ⟶ 614 сек (8%)

Используйте HardSourcePlugin

Плагин для webpack [27], который кеширует собранные модули. Есть большой issue [28] на тему кеширования в webpack, там и зародилась идея этого плагина. По ссылке как раз пост автора плагина.

Подключается в конфиге webpack:

plugins: [
    new HardSourceWebpackPlugin()
]

По умолчанию кеш складывается в node_modules/.cache/hard-source, но можно указать другой каталог.

В моем случае, просто подключение этого плагина без конфига, дало прирост с 200 секунд до 50 (при наличии кеша).

При использовании webpack-dev-server и postcss придется поработать напильником [29].

Замеченные проблемы:

  • иногда не видит изменений.

Разница: 275 сек ⟶ 53 сек (80%)

Используйте webpack-parallel-uglify-plugin

UglifyJS [30] — это инструмент, который используется для минификации JS-кода. Webpack добавляет его в плагины автоматически, если собирать бандл с флагом -p [31].

Для использования webpack-parallel-uglify-plugin [32] есть 2 причины:

  • умеет кешировать;
  • запускает Uglify в параллельных потоках.

Если использовать этот плагин, то придется вручную добавлять его в продакшен-сборку и уже не пользоваться флагом -p. Пример конфига:

plugins: [
    new ParallelUglifyPlugin({
        cacheDir: path.join(dir.root, "node_modules", ".cache", "parallel-uglify"),
        uglifyJS: {/* uglifyjs options */}
    })
]

Разница: 627 сек ⟶ 391 сек (38%)

Не очищайте каталог сборки проекта

Этот пункт следует из описанных выше. Не стоит удалять node_modules перед деплоем клиентского кода.

Используйте DllPlugin

У многих наверняка подключены в проектах библиотеки или фреймворки, которые используются по всему проекту. Такие библиотеки можно каждый раз не пересобирать. С этим нам поможет пара встроенных в 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%)

Используйте css-loader < v0.15

Начиная с версии 0.15 css-loader начал сильно замедлять сборку [38]. Судя по комментариям, у некоторых [39] сборка замедлилась больше, чем в 50 раз. В моем же случае разница была, но не такая большая.

Список фич, которые нельзя будет использовать при понижении версии:

Но CSS Modules и scope вполне можно использовать. Полная документация для версии 0.14.5 [46].

Разница: 213 сек ⟶ 185 сек (13%)

Используйте CommonsChunkPlugin

Этот 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]! И снова экспериментируйте!

Используйте настройку noParse

Эта настройка webpack [52] позволяет избежать парсинга определенных библиотек или файлов. Webpack просто добавит такой модуль в бандл, без преобразования.

То есть, если какая-то библиотека доставляется в npm в минифицированном виде и в ней нет никаких require и, например, кода, который нужно компилировать, ее можно не прогонять через webpack, потому что это будет бесполезно и может быть долго, а сразу засунуть в бандл.

Разница: меньше 10 сек

Используйте настройку cache

Эта настройка webpack [53] позволяет кешировать модули и чанки. Включена по умолчанию в режиме --watch.

Разница: меньше 10 сек

Используйте ContextReplacementPlugin

Этот плагин [54] не столько про скорость сборки, сколько про размер бандла. Некоторые библиотеки [55] при подключении тянут за собой тонну мусора (например, локализации для кучи языков). При помощи ContextReplacementPlugin можно это исправить:

plugins: [
    new webpack.ContextReplacementPlugin(/moment[/\]locale$/, /ru/)
]

При этом у меня один entry point (не минифицированный) похудел с 4.2 Мб до 3.8 Мб (10%). CommonsChunkPlugin был отключен.

Разница: меньше 10 сек

Используйте parallel-webpack

До этого момента мы различными способами ускоряли сборку отдельных 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%)

Используйте HappyPack

Это такой пакет [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