Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
SlideShare a Scribd company logo
Внутреннее устройство
и оптимизация бандла
webpack
Алексей Иванов, Злые марсиане
Злые марсиане
Злые марсиане
Над чем я работаю
Внутреннее устройство и оптимизация бандла webpack
—  React в бандле весит больше чем lib/react.js .
—  Несколько версий lodash или  underscore .
—   Moment.js грузит 100+ локалей.
—  Непонятные полифилы.
—  Не работает tree shaking.
—  Изменяется кеш для неизменившихся чанков.
—  И так далее.
Проблемы
Для кого
этот доклад
—  CommonJS.
—  Резолв путей до файлов.
—  Устройство бандла изнутри.
—  Глобальные константы и DefinePlugin.
—  UglifyJS и dead code elimination.
—  ES6 modules и tree shaking.
—  Выделение чанков и асинхронная подгрузка.
—  Анализ результатов сборки.
План
CommonJS
// module.js
const one = 1;
exports.two = 2;
module.exports = { one };
// othermodule.js
const m = require('module');
console.log(m.one);
console.log(m.two); // ошибка
CommonJS
—  JS-файл с переменными require , exports , module .
—   this равно exports .
—   exports по умолчанию равно {} .
—  Чтобы мы могли использовать npm-модули в браузере, нам нужно
эмулировать в браузере это поведение.
CommonJS
Резолв путей
—   / , ./ , ../ – как в файловой системе.
—  Библиотеки:
—  папка с именем модуля в  node_modules ,
—  если нет в  node_modules , ищем рекурсивно выше по дереву,
—  Добавляем в конец пути .js или  /index.js .
Схема резолва путей в node.js
Схема резолва путей в webpack 2
Резолв путей в require()
./
node_modules/
lodash-5.0.0/
some-lib-1.0.0/
node_modules/
lodash-1.0.0/
index.js
Несколько версий библиотек
ПРОБЛЕМА 1
Базовое
устройство
бандла
function( module, exports, require ) {
const module = require('../path');
// ...
module.exports = ...;
}
CommonJS модуль в браузере
function(module, exports, __webpack_require__ ) {
const module = __webpack_require__(0) ;
// ...
module.exports = ...;
}
CommonJS модуль в браузере
(function(modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
/* код инициализации */
}
// ...
return __webpack_require__(1); // корневой файл
})([ /* массив модулей */ ]);
Как выглядит простой бандл
(function(modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
/* код инициализации */
}
// ...
return __webpack_require__(1) ; // корневой файл
})([ /* массив модулей */ ]);
Как выглядит простой бандл
1.  Ищет инициированный модуль в кэше.
2.  Создает заглушку и добавляет её в массив.
Что делает __webpack_require__
{
i: moduleId,
l: false,
exports: {}
}
Что делает __webpack_require__
1.  Ищет инициированный модуль в кэше.
2.  Создает заглушку и добавляет её в массив.
3.  Выполняет код модуля с  this равным module.exports .
Что делает __webpack_require__
modules[moduleId].call(
module.exports , // exports будет this для модуля
module,
module.exports , // по-умолчанию равно {}
__webpack_require__
);
Что делает __webpack_require__
{
i: moduleId,
l: false,
exports: // тут теперь какой-то код
}
Что делает __webpack_require__
1.  Ищет инициированный модуль в кэше.
2.  Создает заглушку и добавляет её в массив.
3.  Выполняет код модуля с  this равным module.exports .
4.  Возвращает module.exports .
Что делает __webpack_require__
const p = '4';
const m = require("./module" + p );
Require в бандле:
const m = __webpack_require__(3)("./module" + p );
Как работает подключение
модуля по маске
var map = {
" ./module ": 1, " ./module.js ": 1,
" ./module2 ": 0, " ./module2.js ": 0,
}
function webpackContext(...) {/* код выбора модуля */};
module.exports = webpackContext;
Справка по Dependency management
Модуль резолва пути в бандле
require('./locale/' + name); // 118 локалей
Решение:
new webpack.ContextReplacementPlugin(
/moment[/]locale$/,
/ en|ru /
)
moment/moment.js
ПРОБЛЕМА 2
Глобальные
константы
и DefinePlugin
const version = VERSION ;
if ( process.env.NODE_ENV !== "production") {
const module = require('module');
// что-то делаем
}
Константы в коде
new webpack.DefinePlugin({
"process.env": {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
RUBY_ENV: '"production"',
BOOL: "true",
},
VERSION: JSON.stringify(process.env.VERSION),
});
DefinePlugin
const version = "1.0.1" ;
if ( false ) {
const module = require('module'); // не импортирован
// что-то делаем
}
Преобразованный код
// До преобразования
const { NODE_ENV } = process.env ;
// После преобразования
var _process$env = process.env,
NODE_ENV = _process$env.NODE_ENV;
Ссылка на песочницу с Babel
Неточная строка для замены
ПРОБЛЕМА 3
var _process$env = process.env,
NODE_ENV = "production";
if (NODE_ENV !== "production") {
const m = __webpack_require__(0) ;
}
Неточная строка для замены
ПРОБЛЕМА 3
Что будет, если
не заменить
process.env?
process – стандартная переменная в node.js, webpack её заполифилит
для совместимости.
Многие другие переменные из  node.js тоже будут заменены.
Полифилы node.js
ПРОБЛЕМА 4
—  React
—  Redux
—  ...
Функции отладки в библиотеках
ПРОБЛЕМА 5
Сжатие кода
—  Удаляет пробелы.
—  Переименовывает переменные короткими именами.
—  Делает dead code elimination.
Ссылка на песочницу с UglifyJS2
UglifyPlugin
function text() {
var one = 1;
var two = 2;
var three = 3;
return one + two;
}
Было
СОКРАЩЕНИЕ ИМЁН И УДАЛЕНИЕ ПЕРЕМЕННЫХ
function text(){var t=1,e=2
return t+e}
Стало
СОКРАЩЕНИЕ ИМЁН И УДАЛЕНИЕ ПЕРЕМЕННЫХ
var one = 1;
var two = 2;
var three = 3;
console.log(one + two);
Было
А ВОТ ТАК НЕ СРАБОТАЕТ
var one=1,two=2,three=3
console.log(one+two)
Стало
А ВОТ ТАК НЕ СРАБОТАЕТ
if ( true ) {
console.log(1);
}
if ( false ) {
console.log(2);
}
Было
УДАЛЕНИЕ УСЛОВИЙ
console.log(1);
Стало
УДАЛЕНИЕ УСЛОВИЙ
var f = false;
if (f) {
console.log(1);
}
Было
ТАК ТОЖЕ НЕ СРАБОТАЕТ
var f=!1
f&&console.log(1)
Стало
ТАК ТОЖЕ НЕ СРАБОТАЕТ
var _process$env = process.env,
NODE_ENV = "production";
if ( NODE_ENV !== "production" ) {
const m = __webpack_require__(0);
}
Переменные в условиях
ПРОБЛЕМА 6
ES6 modules
// CommonJS
const m = require('./m');
exports.one = 1;
module.exports = 2;
// ES Modules
import m from './m';
export const one = 1;
export default 2;
Импорты и экспорты обязательно иммутабельны.
Документация по import, export
Отличия от CommonJS
В теории webpack может точно определить, что используется у нас
в приложении, и помечать их соответственно.
На практике все несколько сложнее.
Tree shaking
export const method (() => {
window.foo == '111';
})();
Сайд эффекты
TREE SHAKING
// module.js
export const one = 1;
export const two = 2;
// index.js
import { one, two } from './module';
console.log(one);
Пример импорта и экспорта
var __WEBPACK_IMPORTED_MODULE_0__module__
= __webpack_require__(0);
console.log( __WEBPACK_IMPORTED_MODULE_0__module__ ["a"]);
index.js в бандле
const one = 1;
__webpack_exports__["a"] = one;
const two = 2;
// нет __webpack_exports__ за ненадобностью
module.js в бандле
import module3 from './folder/module-3';
// импорт из method попадет в сборку
export const method = () => module3 ;
// export.default пометится как используемый
export default 1223;
Работает только с экспортами
ПРОБЛЕМА 7
Такой код импортирует всю библиотеку:
import { method } from 'lodash-es';
Решение:
—   import { method } from 'lodash-es/method'
—  babel-plugin-lodash
Работает только с экспортами
ПРОБЛЕМА 7
Выключите в Babel обработку modules , чтобы он не менял их
на  require и  exports :
{"presets": [
["latest", {
"es2015": { "modules": false }
}]
]}
Важно!
Чанки
—  Синхронные и асинхронные.
—  В первый файл добавляется функция window["webpackJsonp"] .
—  В следующих файлах вызывается функция webpackJsonp со списком
модулей и id модулей, которые надо запустить.
Чанки
webpackJsonp(
[0], // id чанка
[{ 22: function(...){...} }], // массив модулей
[12] // id модулей для запуска
]);
Чанки
—  Синхронные и асинхронные.
—  В первый файл добавляется функция window["webpackJsonp"] .
—  В следующих файлах вызывается функция webpackJsonp со списком
модулей и id модулей, которые надо запустить.
—  Все модули попадают в общий массив и используются оттуда.
Чанки
import('./module4').then((module4) => {
console.log(module4);
});
Асинхронная подгрузка чанков
__webpack_require__.e (0)
.then(__webpack_require__.bind(null, 1))
.then((module4) => {
console.log(module4);
});
Асинхронная подгрузка чанков
Создание имени чанка при наличии [chunkhash] :
script.src = chunkId + "." + {
"0":" 588290cc688bace070b6 ",
"1":" 5966f9b147ab01778e34 ",
}[chunkId] + ".js";
Имена файлов чанков
CommonsChunk
Plugin
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
filename: '[name].[chunkhash].js',
minChunks: 2
})
Выносим общие модули
в отдельный файл
import() -чанки игнорируются CommonsChunkPlugin .
Чтобы не игнорировались, добавьте:
new webpack.optimize.CommonsChunkPlugin({
...,
children: true
})
Вложенные чанки
ПРОБЛЕМА 8
new webpack.optimize.CommonsChunkPlugin({
name: "vendor",
minChunks: function (module) {
return module.context &&
module.context.indexOf(" node_modules ") !== -1;
}
})
Вынос node_modules
Пример c node_modules не работает для кэша:
1.  При добавлении файлов меняются индексы;
2.  Код загрузки и инициализации живет в первом файле:
—  меняется стартовый индекс,
—  меняются ссылки на чанки.
Изменяющиеся индексы
ПРОБЛЕМА 9
(function(modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
/* код инициализации */
}
// ...
return __webpack_require__(1) ; // корневой файл
})([ /* массив модулей */ ]);
Изменяющиеся индексы
ПРОБЛЕМА 9
Создание имени чанка при наличии [chunkhash] :
script.src = chunkId + "." + {
"0":" 588290cc688bace070b6 ",
"1":" 5966f9b147ab01778e34 ",
}[chunkId] + ".js";
Изменяющиеся индексы
ПРОБЛЕМА 9
—   NamedModulesPlugin() – меняет индексы на пути до файла.
—   HashedModuleIdsPlugin()  — меняет индексы на хэш содержимого.
Фиксируем имена модулей
new webpack.optimize.CommonsChunkPlugin({
name: "manifest",
minChunks: Infinity
}),
Выносим инициализацию
в отдельный файл
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor', ...
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest', ...
})
Несколько CommonsChunkPlugin
Анализ бандла
Строит treemap бандлов. Удобно проверять, не попали ли в бандл:
—  Две версии одной библиотеки.
—  Копии библиотеки в разных чанках.
—  Бибиотеки, которые должны были вырезаться по условию.
—  Непредвиденные зависимости у библиотек.
—  Просто большие файлы.
webpack-bundle-analyzer
Внутреннее устройство и оптимизация бандла webpack
Показывает отношения между файлами в графе — кто на кого
ссылается, кто кого добавил в сборку. Удобно использовать, чтобы
понять:
—  Кто именно использует файл.
—  Кто именно подключил библиотеку.
webpack-runtime-analyzer
Внутреннее устройство и оптимизация бандла webpack
—  Сделайте пустой бандл и посмотрите содержимое, там 40 строчек.
—  Не бойтесь ходить в исходники и смотреть что получилось в коде.
—  После добавления библиотек всегда запускайте анализатор банда
и смотрите что он с собой притащил.
—  После добавления чанков проверяйте их содержимое.
Итого
Алексей Иванов, Злые марсиане
Twitter: @iadramelk
Внутреннее устройство
и оптимизация бандла webpack
Внутреннее устройство и оптимизация бандла webpack

More Related Content

Внутреннее устройство и оптимизация бандла webpack