JS-приложения, сайты и другие ресурсы становятся сложнее и инструменты сборки — это реальность веб-разработки. Бандлеры помогают упаковывать, компилировать и организовывать библиотеки. Один из мощных и гибких инструментов с открытым исходным кодом, который можно идеально настроить для сборки клиентского приложения — Webpack.

Максим Соснов (crazymax11) — Frontend Lead в N1.RU внедрил Webpack в несколько больших проектов, на которых до этого была своя кастомная сборка, и контрибьютил с ним несколько проектов. Максим знает, как с Webpack собрать бандл мечты, сделать это быстро и конфигурировать так, чтобы конфиг оставался чистым, поддерживаемым и модульным.


Расшифровка отличается от доклада — это сильно усовершенствованная пруфлинками версия. По всей расшифровке рассыпаны пасхалочки на статьи, плагины, минификаторы, опции, транспайлеры и пруфы слов докладчика, ссылки на которые просто не поставить в выступление. Если собрать все, то откроется бонусный уровень в Webpack :-)

Интеграция Webpack в типичный проект


Обычно порядок внедрения такой: разработчик где-то прочитал статью про Webpack, решает его подключить, начинает встраивать, как-то это получается, все заводится, и какое-то время webpack-config работает — полгода, год, два. Локально все хорошо — солнце, радуга и бабочки. А потом приходят реальные пользователи:

— С мобильных устройств ваш сайт не загружается.
— У нас все работает. Локально все хорошо!


На всякий случай разработчик идет все профилировать и видит, что для мобильных устройств бандл весит 7 Мбайт и грузится 30 секунд. Это никого не устраивает и разработчик начинает искать, как решить проблему — может подключить лоадер или найти волшебный плагин, который решит все проблемы. Чудесным образом такой плагин находится. Наш разработчик идет в webpack-config, пытается установить, но мешает строчка кода:

if (process.env.NODE_ENV === ’production’) {
    config.module.rules[7].options.magic = true; 
}

Строчка переводится так: «Если config собирается для production, то возьми седьмое правило, и поставь там опцию magic = true». Разработчик не знает, что с этим делать и как решать. Это ситуация, когда нужен бандл мечты.

Как собрать бандл мечты?


Для начала определим, что это такое. Прежде всего, у бандла мечты две основные характеристики:

  • Мало весит. Чем меньше вес — тем быстрее пользователь получит работающее приложение. Вы же не хотите, чтобы ваш сайт открывался 15 секунд.
  • Пользователь загружает только то, что нужно загрузить для показа текущей страницы сайта, и ни байтом больше!

А чтобы уменьшать размер бандла, нужно сначала оценить его размер.

Оценить размер бандла


Самое популярное решение — это плагин WebpackBundleAnalyzer. Он собирает статистику сборки приложения и рендерит интерактивную страничку, на которой можно посмотреть расположение и вес каждого модуля.

image

Если этого мало, можно построить граф зависимостей с помощью другого плагина.

image

Или круговую диаграмму.

image

Если и этого недостаточно, и вы хотите продать Webpack маркетологам, то можно построить целую вселенную, где каждая точка — это модуль, как звезда во Вселенной.

image

Инструментов, которые оценивают размер бандла и следят за ним, очень много. Есть опция в конфиге Webpack, которая рушит сборку, если бандл слишком много весит, например. Есть плагин duplicate-package-checker-webpack-plugin который не даст собрать бандл, если у вас 2 npm-пакета разных версий, например, Lodash 4.15 и Lodash 4.14.

Как уменьшить бандл


  • Самое очевидное — подключить UglifyJS, чтобы он заминифицировал JavaScript.
  • Использовать специальные лоадеры и плагины, которые сжимают и оптимизируют определенный ресурс. Например, css-nano для css, или SVGO, который оптимизирует SVG.
  • Сжимать все файлы прямо в Webpack через gzip/brotli плагины.
  • Другие инструменты.

Теперь поймем как выкинуть лишнее из бандла.

Выкинуть лишнее


Рассмотрим это на популярном примере с moment.js: import moment from 'moment'. Если вы возьмете пустое приложение, импортируете в него moment.js и ReactDOM, и потом пропустите это через WebpackBundleAnalyzer, то увидите следующую картину.

image

Оказывается, когда вы добавляете в дате день, час или просто хотите поставить ссылку «через 15 минут» с помощью moment.js, вы подключаете целых 230 Кбайт кода! Почему так происходит и как это решается?

Загрузка локали в moment


В moment.js есть функция, которая устанавливает локали:

function setLocale(locale) { 
    const localePath = ’locale/’ + locale + ’.js’;
    this._currentLocale = require(localePath);
}

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

image

Решение очень простое — берем стандартный плагин из Webpack и говорим ему: «Если увидишь, что кто-то хочет загрузить много локалей, потому что не может определить какую, — возьми только русскую!»

image

Webpack возьмет только русскую, а WebpackBundleAnalyzer покажет 54 Кb, что уже на 200 Kb легче.

Dead code elimination


Следующая оптимизация, которая нас интересует — Dead code elimination. Рассмотрим следующий код.

const cond = true; 
if (!cond) {
    return false; 
} 
return true; 
someFunction(42);

Большинство строк из этого кода не нужны в финальном бандле — блок с условием не выполнится, функция после return — тоже. Все, что нужно оставить, это return true. Это как раз и есть Dead code elimination: инструмент сборки обнаруживает код, который не может быть выполнен, и вырезает его. Есть приятная особенность, что UglifyJS умеет это делать.

Теперь перейдем к более продвинутому способу Dead code elimination — Tree shaking.

Tree shaking


Допустим, у нас есть приложение, которое использует Lodash. Я сильно сомневаюсь, что кто-то применяет весь Lodash целиком. Скорее всего, эксплуатируется несколько функций типа get, IsEmpty, unionBy или подобных.

Когда мы делаем Tree shaking, мы хотим от Webpack, чтобы он «потряс» ненужные модули и выкинул их, а у нас остались только необходимые. Это и есть Tree shaking.

Как работает Tree shaking в Webpack


Допустим, у вас есть такой код:

import { a } from ’./a.js’; 
console.log(a); 

Код очень простой: из какого-то модуля импортируете переменную a и выводите ее. Но в этом модуле есть две переменные: a и b. Переменная b нам не нужна, и мы хотим ее убрать.

export const a = 3 
export const b = 4 

Когда придет Webpack, он преобразует код с импортом в такой:

var d = require(0);
console.log(d["a"]); 

Наш import превратился в require, а console.log не изменился.

Зависимость Webpack преобразует в следующий код:

var a = 3; 
module.exports["a«] = a; 
/* unused harmony export b */ 
var b = 4;


Webpack оставил экспорт переменной a, и убрал экспорт переменной b, но саму переменную оставил, пометив её специальным комментарием. В преобразованном коде переменная b не используется, и UglifyJS может ее удалить.

Tree shaking в Webpack работает, только если у вас есть какой-нибудь минификатор кода, например, UglifyJS или babel-minify.

Рассмотрим случаи интереснее — когда Tree shaking не работает.

Когда Tree shaking не работает


Кейс № 1. Вы пишете код:

module.exports.a = 3; 
module.exports.b = 4;

Прогоняете код через Webpack, и он остается таким же. Все потому, что бандлер организует Tree shaking, только если вы используете ES6 модули. Если применяете CommonJS модули, то Tree shaking работать не будет.

Кейс № 2. Вы пишете код с ES6 модулями и именованными экспортами.

export const a = 3 
export const b = 4

Если ваш код прогоняется через Babel и вы не выставили опцию modules в false, то Babel приведет ваши модули к CommonJS, и Webpack опять же не сможет выполнить Tree shaking, ведь он работает только с ES6 модулями.

module.exports.a = 3; 
module.exports.b = 4;

Соответственно, нам нужно быть уверенными, что никто в нашем пайплане сборки не будет транспайлить ES6 модули.

Кейс № 3. Допустим, у нас есть такой бесполезный класс, который ничего не делает: export class ShakeMe {}. Более того, мы его еще и не используем. Когда Webpack будет проходить по импортам и экспортам, Babel превратит класс в функцию, а бандлер пометит, что функция не используется:

/* unused harmony e[port b */
var ShakeMe = function () { 
    function ShakeMe() { 
        babelHelpers.classCallCheck(this, ShakeMe); 
}

    return ShakeMe; 
}();

Вроде все должно быть хорошо, но если приглядимся, то увидим, что внутри этой функции есть глобальная переменная babelHelpers, из которой вызывается какая-то функция. Это сайд-эффект: UglifyJS видит, что вызывается какая-то глобальная функция и не вырежет код, потому что боится, что что-то сломает.

Когда вы пишете классы и прогоняете их через Babel, они никогда не вырезаются. Как это исправляется? Есть стандартизованный хак — добавить коммент /*#__PURE__*/ перед функцией:

/* unused harmony export b */ 
var ShakeMe = /*#__PURE__*/ function () { 
    function ShakeMe() { 
        babelHelpers.classCallCheck(this, ShakeMe); 
}

    return ShakeMe; 
}();

Тогда UglifyJS поверит на слово, что следующая функция чистая. К счастью, сейчас это делает Babel 7, а в Babel 6 до сих пор ничего не удаляется.

Правило: если у вас где-то есть сайд-эффект, то UglifyJS ничего не сделает.

Подведем итоги:

  • Tree shaking не работает для большинства библиотек из npm, потому что они все из CommonJS и собираются старым Babel’ем.
  • Скорее всего, Tree shaking будет адекватно работать для тех библиотек, которые уже к этому подготовлены, например, Lodash-es, Date-fns и ваш код или библиотеки.
  • В сборке участвует UglifyJS.
  • Используются ES6-модули.
  • Нет сайд-эффектов.

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

Загружаем только нужный функционал


Эту часть разобьем на две. В первой части загружается только код, который требует пользователь: если пользователь заходит на главную страницу вашего сайта, он не загружает страницы личного кабинета. Во второй, правки в коде приводят к минимально возможной перезагрузке ресурсов.

Загружаем только необходимый код


Рассмотрим структуру воображаемого приложения. В нем есть:

  • Entry-point — APP.
  • Три страницы: главная, поиск и карточка.

image

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

image

Когда пользователь придет на поиск с главной страницы, он будет перезагружать и квадратик, и кружок второй раз, хотя они у него уже есть. В идеале мы хотели бы видеть примерно это.

image

Хорошо, что в Webpack 4 уже есть встроенный плагин, который это делает за нас — SplitChunksPlugin. Плагин выносит код приложения или код node modules, который используется несколькими чанками в отдельный чанк, при этом гарантирует, что чанк с общим кодом будет больше 30 Kb, а для загрузки страницы требуется загрузить не больше 5 чанков. Стратегия оптимальна: слишком маленькие чанки загружать невыгодно, а загрузка слишком большого количества чанков — долго и не так эффективно, как загрузка меньшего количества чанков даже на http2. Чтобы повторить такое поведение на 2 или 3 версии Webpack, приходилось писать 20–30 строк с не документированными фичами. Сейчас это решается одной строкой.

Вынос CSS


Было бы прекрасно, если бы мы еще вынесли CSS для каждого чанка в отдельный файл. Для этого есть готовое решение — Mini-Css-Extract-Plugin. Плагин появился только в Webpack 4, а до него не было адекватных решений для такой задачи — только хаки, боль и простреленные ноги. Плагин выносит CSS из асинхронных чанков и создан специально для этой задачи, которую выполняет идеально.

Минимально возможная перезагрузка ресурсов


Разберемся, как бы нам сделать так, чтобы при релизе, например, нового промо-блока на главной странице пользователь перезагружал бы минимально возможную часть кода.

Если бы у нас было версионирование — всё было бы хорошо. Вот у нас главная страница версии N, а после релиза промо-блока — версии N+1. Webpack предоставляет подобный механизм прямо из коробки с помощью хэширования. После того, как Webpack соберет все ассеты, — в данном случае app.js, — то посчитает его контент-хэш, и добавит его к имени файла, чтобы получилось app.[hash].js. Это и есть версионирование, которое нам нужно.

image

Давайте теперь проверим как это работает. Включим хэши, внесем правки на главной странице, и посмотрим — действительно ли изменился код только главной страницы.Мы увидим, что изменились два файла: main и app.js.

image

Почему так произошло, ведь это нелогично? Чтобы понять почему, давайте разберем app.js. Он состоит из трех частей:

  • код приложения;
  • webpack runtime;
  • ссылки на асинхронные чанки.

Когда мы меняем код в main, меняется его контент и хэш, а значит, в app меняется и ссылка на него. Сам app тоже поменяется и его нужно перезагрузить. Решение этой проблемы — разделить app.js на два чанка: код приложения и webpack runtime и ссылки на асинхронные чанки. Webpack 4 делает все за нас одной опцией runtimeChunk, которая весит очень мало — меньше 2 Кбайта в gzip. Перезагрузить его для пользователя — практически ничего не стоит. RuntimeChunk включается всего одной опцией:

optimization: { 
    runtimeChunk: true 
} 

В Webpack 3 и 2 мы бы написали 5-6 строк, вместо одной. Это не сильно больше, но все равно лишнее неудобство.

image

Все здорово, мы научились выносить ссылки и рантайм! Давайте напишем новый модуль в main, зарелизим, и — оп! — теперь вообще все перезагружается.

image

Почему так? Давайте разберемся, как работают модули в webpack.

Модули в webpack


Допустим, есть такой код, в котором вы добавляете модули a, b, d и e:

import a from ’a’;
import b from ’b’;
import d from ’d’;
import e from ’e’;

Webpack преобразует импорты в require: a, b, d и e заменились на require(0), require (1), require (2) и require (3).

var a = require(0); 
var b = require(1);
var d = require(2); 
var e = require(3);

Представим картину, которая очень часто случается: вы пишете новый модуль c import c from 'c'; и вставляете его где-то посередине:

import a from ’a’; 
import b from ’b’; 
import c from ’c’;
import d from ’d’; 
import e from ’e’;

Когда Webpack будет все обрабатывать, то преобразует импорт нового модуля в require(2):

var a = require(0); 
var b = require(1);
var c = require(2);
var d = require(3); 
var e = require(4);

Модули d и e, которые были 2 и 3, получат цифры 3 и 4 — новые id. Из этого следует простой вывод: использовать порядковые номера как id немного глупо, но Webpack это делает.

Не используйте порядковый номер как уникальный id

Для исправления проблемы есть встроенное решение Webpack — HashedModuleIdsPlugin:

new webpack.HashedModuleIdsPlugin({ 
    hashFunction: ’md4?, 
    hashDigest:’base64?, 
    hashDigestLength: 4,
}), 

Этот плагин вместо цифровых id использует 4 символа md4-хэша от абсолютного пути до файла. С ним наши require превратятся в такие:

var a = require(’YmRl’);
var b = require(’N2Fl’);
var c = require(’OWE4?); 
var d = require(’NWQz’);
var e = require(’YWVj’);

Вместо цифр появились буквы. Конечно, есть скрытая проблема — это коллизия хэшей. Мы на нее натыкались один раз и можем советовать использовать 8 символов, вместо 4. Настроив хэши правильно, все будет работать так, как мы изначально и хотели.

Мы теперь знаем, как собирать бандл мечты.

  • Минифицировать.
  • Использовать код-сплиттинг.
  • Настроить хэши.

Собирать научились, а теперь поработаем над скоростью.

Как собрать бандл мечты быстро?


У нас в N1.RU самое большое приложение состоит из 10 000 модулей и без оптимизаций собирается 28 минут. Мы смогли ускорить сборку до двух минут! Как же мы это сделали? Существует 3 способа ускорения любых вычислений, и все три применимы к Webpack.

Параллелизация сборки


Первое, что мы сделали — распараллелили сборку. Для этого у нас есть:

  • HappyPackPlugin, который оборачивает ваши лоадеры в другие лоадеры, и выносит все вычисления, которые обернули, в отдельные процессы. Это позволяет, например, распараллелить Babel и node-sass.
  • thread-loader. Выполняет примерно то же, что и HappyPackPlugin, только использует не процессы, а thread pool. Переключение на отдельный тред — затратная операция, используйте осторожно, и только если хотите обернуть ресурсоемкие и тяжелые операции, типа babel или node-sass. Для загрузки json, например, параллелизация не нужна, потому что он грузится быстро.
  • В используемых вами плагинах и лоадерах, скорее всего, уже есть встроенные инструменты параллелизации — стоит только посмотреть. Например, эта опция есть в UglifyJS.

Кэширование результатов сборки


Кэшировать результаты сборки — наиболее эффективный способ ускорения сборки Webpack.

Первое решение, которое у нас есть — cache-loader. Это лоадер, который встает в цепочку лоадеров и сохраняет на файловую систему результат сборки конкретного файла для конкретной цепочки лоадеров. При следующей сборке бандла, если этот файл есть на файловой системе и уже обрабатывался с этой цепочкой, cache-loader возьмет результаты и не будет вызывать те лоадеры, которые стоят за ними, например, Babel-loader или node-sass.

На графике представлено время сборки. Синий столбик — 100% время сборки, без cache-loader, а с ним — на 7% медленнее. Это происходит потому что cache-loader тратит дополнительное время на сохранение кэшей на файловую систему. Уже на а второй сборке мы получили ощутимый профит — сборка прошла в 2 раза быстрее.

image

Второе решение более навороченное — HardSourcePlugin. Основное отличие: cache-loader — это просто лоадер, который может оперировать только в цепочке лоадеров кодом или файлами, а HardSourcePlugin имеет почти полный доступ к экосистеме Webpack, умеет оперировать другими плагинами и лоадерами, и сам немного расширяет экосистему для кэширования. На графике выше видно, что на первом запуске время сборки увеличилось на 37%, но ко второму запуску со всеми кэшами мы ускорились в 5 раз.

image

Самое приятное, что можно использовать оба решения вместе, что мы в N1.RU и делаем. Будьте осторожны, потому что с кэшами есть проблемы, о которых я расскажу чуть позже.

В уже используемых вами плагинах/лоадерах могут быть встроенные механизмы кэширования. Например, в babel-loader очень эффективная система кэширования, но почему-то по умолчанию она выключена. Такой же функционал есть в awesome-typeScript-loader. В UglifyJS плагине тоже есть кэширование, которое замечательно работает. Нас он ускорил на несколько минут.

А теперь проблемы.

Проблемы кэширования


  • Кэш может неправильно валидироваться.
  • Примененные решения могут не работать с подключенными плагинами, лоадерами, вашим кодом или друг с другом. В этом плане cache-loader — простое и беспроблемное решение. А вот с HardSourcePlugin нужно быть внимательнее.
  • Сложно дебажить, если всё сломалось. Когда кэширование сработает неправильно и произойдет непонятная ошибка, будет очень сложно разобраться, в чем же проблема.

На чем сэкономить в production?


Последний способ ускорить какой-либо процесс — не делать какие-то части процесса. Давайте подумаем, на чем можно сэкономить в production? Что мы можем не делать? Ответ короткий — мы ничего не можем не делать! Мы не вправе отказаться от чего-то в production, но можем хорошо сэкономить в dev.

На чем экономить:

  • Не собирать source map, пока они нам не понадобятся.
  • Использовать style-loader вместо крутой схемы с выносом css и с обработкой через css-лоадеры. Style-loader сам по себе очень быстрый, потому что он берет строчку css и загоняет ее в функцию, которая вставляет эту строчку в тэг style.
  • Можно оставить в browserlist только используемый конкретно вами браузер — скорее всего это last chrome. Это позволит сильно ускориться.
  • Полностью отказаться от какой-либо оптимизации ресурсов: от UglifyJS, css-nano, gzip/brotli.

Ускорение сборки — это параллелизация, кэширование и отказ от вычислений. Выполнив эти три простых шага, вы можете ускориться очень сильно.

Как конфигурировать Webpack?


Мы разобрались, как собрать бандл мечты и как собрать его быстро, а теперь разберемся как сконфигурировать Webpack, чтобы при этом не стрелять себе в ногу каждый раз при изменении конфига.

Эволюция конфига в проекте


Типичный путь webpack-конфиг в проекте начинается с простого конфига. Сначала вы просто вставляете Webpack, Babel-loader, sass-loader и все хорошо. Потом, неожиданно, появляются какие-то условия на process.env, и вы вставляете условия. Одно, второе, третье, все больше и больше, пока не добавляется условие с «магической» опцией. Вы понимаете, что все уже совсем плохо, и лучше просто продублировать конфиги для dev и production, и сделать правки два раза. Все будет понятнее. Если у вас мелькнула мысль: «Что-то здесь не так?», то единственный работающий совет — держать конфиг в порядке. Расскажу, как мы это делаем.

Держать конфиг в порядке


Мы используем пакет webpack-merge. Это npm-пакет, который создан, чтобы объединять несколько конфигов в один. Если вас не устраивает стратегия объединения по умолчанию, то можно кастомизировать.

Структура проекта с конфигом


У нас есть 4 основные папки:

  • Loaders.
  • Plugins.
  • Presets.
  • Parts.

Расскажу про каждую отдельно.

Plugin/Loader


Это папки, которые содержат файлы для каждого лоадера и плагина, с подробной документацией и более человеческим API, чем тот, что предоставляют разработчики плагинов и лоадеров.

Выглядит это примерно так:

/** 
    * Подробный JSdoc 
    * @param {Object} options 
    * @see ссылка на доки 
    */ 
module.exports = function createPlugin(options) { 
    return new Plugin(options); 
}; 

Есть модуль, он экспортирует функцию, которая имеет опции, и есть документация. На словах выглядит хорошо, а в реальности наши доки к url-loader выглядят так:

/** 
* url-loader это надстройка над file-loader. Он позволяет учитывать ассеты во время бандлинга 
* 
* @example 
* Какой-то ресурс запросил some-image.png. Если для загрузки нужен url-loader, то url-loader проверит размер файла 
* 1. если он меньше лимита, то url-loader вернет ресурс как base64 строку 
* 2. иначе, url-loader сложит файл в outputPath + name и вернёт вместо ресурса ссылку, по которой его можно загрузить. 
* В случае с some-image.png, он может сохраниться в outputPath/images/some-image.12345678hash.png, а url-loader вернет 
* publicPath/images/some-image.12345678hash.png
* 
* @param {string} prefix префикс имён файлов
* @param {number} limit если ресурс меньше лимита, он будет заинлайнен 
* @return {Object} loader конфиг лоадера 
* @see https://www.npmjs.com/package/url-loader
*/ 

Мы рассказываем в простой форме, что он делает, как работает, описываем, какие параметры принимают функции, что создает лоадер, и даем ссылку на доки. Я надеюсь, что тот, кто сюда зайдет, точно поймет, как работает url-loader. Сама функция выглядит так:

function urlLoader(prefix = ’assets’, limit = 100) {
    return { 
        loader: ’url-loader’, 
        options: { 
            limit, 
            name: `${prefix}/[name].[hash].[ext]` 
        } 
    }; 
};

Мы принимаем два параметра и возвращаем описание от лоадера. Не следует бояться того, что папка Loader будет громоздкой и на каждый лоадер будет по файлу.

Preset


Это набор опций webpack. Они отвечают за одну функциональность, при этом оперируют лоадерами и плагинами, которые мы уже описали, и настройками webpack, которые у него есть. Самый простой пример — это пресет, который говорит, как правильно загружать scss-файлы:

{ 
    test: /\.scss$/, 
    use: [cssLoader, postCssLoader, scssLoader] 
}

Он использует уже преподготовленные лоадеры.

Part


Части — это то, что уже лежит в самом приложении. Они настраивают точку входа и выхода вашего приложения, и могут регулировать или подключать специфичные плагины, лоадеры и опции. Типичный пример, где мы объявляем точку входа и выхода:

entry: { 
    app: ’./src/Frontend/app.js’ 
}, 
output: { 
    publicPath: ’/static/cabinet/app/’, 
    path: path.resolve(’www/static/app’) 
},

В своей практике мы используем:

  • Базовый пресет, в котором описываем, как загружать шаблоны, json, какие плагины нужно использовать всегда, например, splitChunks.
  • Пресет для dev, где описано, как правильно загружать js/css и плагины на оптимизацию
  • Part, который описывает output, publicPath, entry-point и некоторые специфичные правила, например, как отдельно переписываются source map.

image

Webpack-merge просто выдает нам готовый конфиг. С этим подходом у нас всегда есть документация к конфигурации, в которой достаточно просто разобраться. С webpack-merge мы не лазим по 3-7 конфигам, чтобы поправить везде Babel-loader, потому что у нас есть консистентная конфигурация отдельных частей по всему проекту. А еще интуитивно понятно, где делать правку.

Управление конфигом


Подведем итоги. Используйте готовые инструменты, а не стройте велосипеды. Документируйте решения, потому что webpack конфиги правятся редко и разными людьми — поэтому документация там очень важна. Разделяйте и переиспользуйте то, что пишете.

Теперь вы знаете, как собирать бандл мечты!

Это доклад — один из лучших на Frontend Conf. Понравилось, и хотите больше — подпишитесь на рассылку, в которой мы собираем новые материалы и даем доступ к видео, и приходите на Frontend Conf РИТ++ в мае.

Хотите рассказать миру что-то крутое по фронтенду из своего опыта? Подавайте доклады на FrontenConf РИТ++, который пройдет 27 и 28 мая в Сколково. Присылайте тезисы до 27 марта, а до 15 апреля ПК примет решение о включении доклада в программу конференции. Мы ждем ваш опыт — откликайтесь!

Комментарии (13)


  1. Juma
    14.02.2019 16:31

    самое большое приложение состоит из 10 000 модулей и без оптимизаций собирается 28 минут. Мы смогли ускорить сборку до двух минут!
    Как часто и в каких случаях у вас запускается эта сборка?


    1. crazymax11
      14.02.2019 16:46

      Каждый раз, когда нужно собрать приложение.
      Локально на машине разработчика проект собирается каждый день, но локальный запуск не такой ресурсоёмкий — в dev сборке нет оптимизаций и можно собирать только нужные для разработки части приложения. В CI проект собирается от нескольких раз до нескольких десятков раз за день.


  1. freezlite
    14.02.2019 17:38
    -1

    У нас в N1.RU самое большое приложение состоит из 10 000 модулей

    А это нормально в браузер столько модулей?


    1. crazymax11
      15.02.2019 13:47

      Столько модулей на одну страницу — это конечно же ужасно. Но в бандле мечты конечно же не так ;) Статистика сборки вебпака говорит, что для сборки нашего самого большого приложения задействовано 10 000 модулей. Само же приложение содержит кучу страниц и эти 10 000 модулей по ним размазаны. В самом докладе собственно про это и рассказывается


  1. HAbRAhabp
    15.02.2019 14:20
    +2

    Можно пойти еще дальше и использовать Preact вместо React и date-fns вместо moment. Я еще использую ES6 / ES5 бандлы в зависимости от браузера — размер становится еще меньше. Полезная статья: https://github.com/GoogleChromeLabs/webpack-libs-optimizations


    1. saggid
      16.02.2019 12:50

      Можно кстати во многих случаях вообще обойтись нативным JS интерфейсом new Date. Пока что в текущем проекте так и работаем, и в целом всё нормально.


      1. gnaeus
        16.02.2019 14:09

        moment часто бывает нужен для third-party библиотек. И тут никуда не денешься. Остается только надеяться, что он рано или поздно отомрет, как jQuery.


        1. HAbRAhabp
          16.02.2019 20:20

          Только если это не будет так больно решать средствами браузера, что вряд ли произойдет в ближайшее время. Есть очень многие вещи (например, format / subtract / startOf), которые решают задачу намного быстрее, чем это делать с нативным Date.
          Вопрос в том, как часто его придется использовать. Для простого бложика будет проще написать функцию, которая будет форматировать дату публикации, чем тащить за собой весь moment.


          1. gnaeus
            17.02.2019 10:55

            Как уже писали выше — есть же date-fns. Импортируешь только нужные функции, а дальше Tree Shaking делает свое дело. moment сейчас это действительно тяжелое легаси и не более того.


  1. CyberAP
    15.02.2019 15:39

    Вопросы про SVG порадовали. Неужели люди настолько часто добавляют\удаляют иконки в проекте что для этого нужен вебпак? Какой-то вебпак головного мозга получается. Можно же просто руками собрать спрайт, вместо раскидывания всех иконок по проекту. Можно сделать несколько таких спрайтов для разных частей приложения. Проблема высосана из пальца на самом деле и получается что инструмент диктует решение, а не здравый смысл.

    А теперь к позитивному: сталкивались ли с FOUC при асинхронных стилях и как лечили?


    1. crazymax11
      16.02.2019 05:04
      +1

      сталкивались ли с FOUC при асинхронных стилях и как лечили?

      Как я понимаю, вопрос про загрузку стилей при SPA переходе с одного чанка на другой. При SPA переходе мы загружаем стили и JS, если на момент загрузки JS стили еще не загружены, мы ждем не больше 300мс их дозагрузки и рендеримся. Это не полностью решает FOUC, но в целом стало немного лучше.


      Для мобильной версии сайта у нас все стили идут инлайном в html разметку, как критический CSS. Там очень мало стилей и мы решили их просто зашить в html, чтобы не иметь никаких проблем с FOUC.


  1. saggid
    16.02.2019 12:44
    +1

    Спасибо за доклад, много интересных рекомендаций.


    А подставлять в html только нужные js и css чанки на этапе серверсайд рендера ещё не пробовали?) Я примерно пол года проходил этот квест, но в итоге всё получилось..


    Начинается всё примерно здесь: https://github.com/faceyspacey/react-universal-component.
    И оно того стоит, да.


    1. crazymax11
      16.02.2019 17:20
      +1

      Мы тоже успешно прошли этот квест ;) но только у нас vue.js и готовых решений не было. Если вкратце, со стороны SSR мы отдаем:


      • app.js — ядро приложения
      • common.js — общие части для многих чанков
      • manifest.js — тот самый рантайм вебпака из доклада
      • чанк текущей страницы
      • app.css
      • common.css
      • стили текущей страницы
      • Если есть критические стили для страницы, то их вшиваем в html в тэг <style>, а все стили указанные в предыдущих пунктах грузим неблокирующим способом. Примерно как описано здесь