От Google — Оптимизация веб-производительности с помощью Webpack

Webpack
От Google — Оптимизация веб-производительности с помощью Webpack

оригинальныйDevelopers.Google.com/Web/Женщины большие…

авторAddy Osmani,Ivan Akulov

Введение

авторAddy Osmani

Современные веб-приложения часто используютbunding toolЧтобы создавать упакованные файлы (например, сценарии, стили и т. д.) для производственных сред, упакованные файлы необходимо оптимизировать и сжать до минимума, чтобы пользователи могли загружать их быстрее. В этой статье мы будем использоватьwebpackчтобы объяснить, как оптимизировать ресурсы веб-сайта во всем. Это поможет пользователям быстрее загрузить ваше приложение и улучшить его работу.

webpack-logo

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

code-splitting

Примечание. Мы создали приложение для упражнений, чтобы продемонстрировать приведенные ниже описания этих оптимизаций. Постарайтесь как можно больше времени практиковать эти советы.webpack-training-project

Начнем с одного из самых ресурсоемких аспектов современных веб-приложений.JavascriptНачинать.

  • Уменьшить громкость фронтенда
  • Воспользуйтесь преимуществом долгосрочного кэширования
  • Мониторинг и анализ приложений
  • Суммировать

Уменьшить размер внешнего интерфейса

авторIvan Akulov

Когда вы оптимизируете приложение, первое, что нужно сделать, это сделать его как можно меньше. Далее следует использоватьwebpackКак сделать.

Включить минификацию

Минимизация — это сжатие кода за счет удаления лишних пробелов, сокращения имен переменных и т. д. Например:

// Original code
function map(array, iteratee) {
let index = -1;
const length = array == null ? 0 : array.length;
const result = new Array(length);
 
while (++index < length) {
result[index] = iteratee(array[index], index, array);
}
return result;
}
 

// Minified code
function map(n,r){let t=-1;for(const a=null==n?0:n.length,l=Array(a);++t<a;)l[t]=r(n[t],t,n);return l} ``` Webpack 支持两种方式最小化代码:UglifyJS 插件和_loader-specific options_。他们可以同时使用。 [The UglifyJS plugin](https://github.com/webpack-contrib/uglifyjs-webpack-plugin)在 bundle 层级中起作用,在编译之后压缩 bundle。下面来展示如何工作: 1.代码: ```javascript // comments.js import './comments.css'; export function render(data, target) { console.log('Rendered!'); } ``` 2.Webpack 打包后大概是下面这样: ```javascript // bundle.js (part of) "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony export (immutable) */ __webpack_exports__["render"] = render; /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css__ = __webpack_require__(1); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css_js___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__comments_css__); function render(data, target) { console.log('Rendered!'); } ``` 3.使用 UglifyJS 插件压缩最小化后大概是下面这样: ```javascript // minified bundle.js (part of) "use strict";function t(e,n){console.log("Rendered!")} Object.defineProperty(n,"__esModule",{value:!0}),n.render=t;var o=r(1);r.n(o) ``` 插件集成在 webpack 中,把它的配置在`plugins`中就可以启用: ```javascript // webpack.config.js const webpack = require('webpack'); module.exports = { plugins: [ new webpack.optimize.UglifyJsPlugin(), ], }; ``` 第二种方式_loader-specific options_ 利用 loader options,可以压缩 Uglify 插件无法最小化的部分。举例,当你利用`css-loader`引入一个 CSS 文件时,文件会编译成一个字符串: ```css /* comments.css */ .comment { color: black; } ``` ↓ ```javascript // minified bundle.js (part of) exports=module.exports=__webpack_require__(1)(), exports.push([module.i,".comment {\r\n color: black;\r\n}",""]); ``` UglifyJS 不能压缩字符串。要压缩这段 css 内容,需要配置 _loader_ : ```javascript // webpack.config.js module.exports = { module: { rules: [ { test: /\.css$/, use: [ 'style-loader', { loader: 'css-loader', options: { minimize: true } }, ], }, ], }, }; ``` &gt; Note: UglifyJS 插件不能编译 ES2015+(ES2016),这意味着如果你的 diamante 中使用类、箭头函数和一些新特性语法,不能编译成 ES5,插件会抛异常。
> 如果需要编译新语法,要使用 [uglifyjs-webpack-plugin](https://github.com/webpack-contrib/uglifyjs-webpack-plugin) 包。也是集成在 webpack 中相同的插件,但是更新一些,能够有能力编译 ES2015+。
 
#### Further reading
 
* [The UglifyJsPlugin docs](https://github.com/webpack-contrib/uglifyjs-webpack-plugin)
* Other popular minifiers: [Babel Minify](https://github.com/webpack-contrib/babel-minify-webpack-plugin), [Google Closure Compiler](https://github.com/roman01la/webpack-closure-compiler)
 
### Specify `NODE_ENV=production` 明确生产环境信息
 
减小前端体积的另外一个方法就是在代码中将`NODE_ENV`[环境变量](https://superuser.com/questions/284342/what-are-path-and-other-environment-variables-and-how-can-i-set-or-use-them)设置成`production`。
 
Libraries 会读取`NODE_ENV`变量判断他们应该在那种模式下工作 - 开发模式 or 生成模式。很多库会基于这个变量有不同的表现。举个例子,当`NODE_ENV`没有设置成`production`,Vue.js 会做额外的检查并且输出一些警告:
 
```javascript
// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.');
}
// …
 

React аналогичен — сборка в режиме разработки с некоторыми предупреждениями:

// react/index.js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
 
// react/cjs/react.development.js
// …
warning$3(
componentClass.getDefaultProps.isReactClassApproved,
'getDefaultProps is only used on classic React.createClass ' +
'definitions. Use a static property named `defaultProps` instead.'
);
// …
 

Эти проверки и предупреждения обычно не нужны в производственной среде, но они остаются в коде и увеличивают размер библиотеки. Настроив веб-пакетDefinePluginудалять:

// webpack.config.js
const webpack = require('webpack');
 
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"',
}),
new webpack.optimize.UglifyJsPlugin(),
],
};
 

DefinePluginЗаменяет все существующие переменные спецификации указанными переменными. Используйте следующую конфигурацию:

1.DefinePluginбуду использовать"production"заменить наprocess.env.NODE_ENV:

// vue/dist/vue.runtime.esm.js
if (typeof val === 'string') {
name = camelize(val);
res[name] = { type: null };
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.');
}
 

// vue/dist/vue.runtime.esm.js
if (typeof val === 'string') {
name = camelize(val);
res[name] = { type: null };
} else if ("production" !== 'production') {
warn('props must be strings when using array syntax.');
}
 

Примечание. Если вы предпочитаете настраивать переменные через интерфейс командной строки, вы можете посмотретьEnvironmentPlugin. это иDefinePluginАналогично, но читает окружение и автоматически заменяетprocess.envвыражение.

2.UglifyJSудалит всеifветка - потому что"production" !== 'production'Всегда возвращайте false , плагин понимает, что ветвь решения внутри кода никогда не будет выполнена:

// vue/dist/vue.runtime.esm.js
if (typeof val === 'string') {
name = camelize(val);
res[name] = { type: null };
} else if ("production" !== 'production') {
warn('props must be strings when using array syntax.');
}
 

// vue/dist/vue.runtime.esm.js (without minification)
if (typeof val === 'string') {
name = camelize(val);
res[name] = { type: null };
}
 

Примечание. Не обязательно использоватьUglifyJSPlugin. Вы можете использовать другой инструмент минимизации, эти страницы поддерживают удаление мертвого кода (например,Babel Minify plugin or the Google Closure Compiler plugin)

Further Reading

Используйте модули ES Используйте модули ES

Используйте следующий методES modulesУменьшите громкость фронтенда.

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

1. Файл с несколькими экспортами, но приложению нужен только один из них:

// comments.js
export const render = () => { return 'Rendered!'; };
export const commentRestEndpoint = '/rest/comments';
 
// index.js
import { render } from './comments.js';
render();
 

2. понимание веб-пакетаcommentRestEndPointНе используется и не может генерировать отдельные экспорты в пакете:

// bundle.js (part that corresponds to comments.js)
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
const render = () => { return 'Rendered!'; };
/* harmony export (immutable) */ __webpack_exports__["a"] = render;
 
const commentRestEndpoint = '/rest/comments';
/* unused harmony export commentRestEndpoint */
})
 

3.UglifyJSPluginУдалить бесполезные переменные:

// bundle.js (part that corresponds to comments.js)
(function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})
 

Если все они написаны с модулями ES, это также вступит в силу, когда они будут сосуществовать с некоторыми библиотеками.

Примечание. В веб-пакете встряхивание дерева не работает без минификатора. webpack удаляет только неиспользуемые переменные экспорта;UglifyJSPluginБесполезный код будет удален. Поэтому, если вы не используете минификатор при компиляции и упаковке, объем не будет меньше после упаковки. Вы также можете не обязательно использовать этот плагин. Другие минимальные плагины также поддерживают удаление мертвого кода (например:Babel Minify plugin or Google Closure Compiler plugin)

Предупреждение: не компилируйте модули ES в CommonJS. Если вы используете Бабельbabel-preset-env or babel-preset-es2015, чтобы проверить текущую конфигурацию. По умолчанию ЕСimport and export to CommonJS require and module.exports. установив возможность отключитьPass the { modules: false } option.

Futher reading

Оптимизация изображений

Картинка в основном будет занимать больше половины объема страницы. Хотя они не так важны, как JavaScript (например, они не блокируют отрисовку страницы), изображения по-прежнему занимают значительную часть вашей пропускной способности. использоватьurl-loader,svg-url-loaderа такжеimage-webpack-loaderоптимизировать в webpack.

url-loaderПозволяет упаковывать небольшие статические файлы в приложение. Без настройки ему нужно передать файл, поместить его в скомпилированный пакет и вернуть URL-адрес этого файла. Однако, если мы укажемlimitвариант, он будет кодировать в меньший файл URL-адрес файла base64. Можно помещать изображения в код Javascript при сохранении HTTP-запросов:

// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif)$/,
loader: 'url-loader',
options: {
// Inline files smaller than 10 kB (10240 bytes)
limit: 10 * 1024,
},
},
],
}
};
 

// index.js
import imageUrl from './image.png';
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`
 

Примечание. Встроенные изображения уменьшают количество независимых запросов, что является хорошим способом (even with HTTP/2), но увеличит время загрузки пакета и преобразования, а также потребление памяти. Убедитесь, что вы не встраиваете изображения слишком большого размера или слишком много изображений, иначе дополнительное время сборки затмит преимущества создания встроенных изображений.

svg-url-loaderа такжеurl-loaderАналогично - оба будут использоватьURL encodingкодировать файл. Это хорошо работает для изображений SVG — поскольку файлы SVG являются текстовыми, кодирование более эффективно по объему:

// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
loader: 'svg-url-loader',
options: {
// Inline files smaller than 10 kB (10240 bytes)
limit: 10 * 1024,
// Remove the quotes from the url
// (they’re unnecessary in most cases)
noquotes: true,
},
},
],
},
};
 

Примечание. В svg-url-loader есть опции для улучшения поддержки IE, но хуже в других браузерах. Если вам нужна совместимость с браузером IE,Установите параметр iesafe: true

image-webpack-loaderСжимайте изображения, чтобы сделать их меньше. Он поддерживает JPG, PNG, GIF и SVG, так как мы будем использовать их все.

Этот загрузчик не встраивает изображения в приложение, поэтому он должен бытьurl-loaderа такжеsvg-url-loaderС использованием. Чтобы избежать копирования и вставки в одни и те же правила (одно для изображений JPG/PNG/GIF и одно для изображений SVG), давайте воспользуемсяenforce: preНакройте этот погрузчик как одно правило:

// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/,
loader: 'image-webpack-loader',
// This will apply the loader before the other ones
enforce: 'pre',
},
],
},
};
 

Настройка по умолчанию на загрузчике должна соответствовать спросу - но если вы хотите углубиться в конфигурацию, см.the plugin options. Чтобы точно определить, какие варианты выбрать, ознакомьтесь с книгой Эдди Османи.guide on image optimization

Further reading

Оптимизировать зависимости

В среднем более половины размера Javascript приходится на зависимости, и ни одна из них может не понадобиться.

Например, Lodash (v4.17.4) добавляет в пакеты 72 КБ минимизированного кода. Но если вы используете только 20 его методов, около 65 КБ кода бесполезны.

Другой пример — Moment.js. Версия V2.19.1 в свернутом виде весит 223 КБ, что очень много — по состоянию на октябрь 2017 года средний размер Javascript на странице составляет 452 КБ. Однако размер локального файла составляет 170 КБ. Если вы не используете многоязычную версию Moment.js, эти файлы непреднамеренно раздуют пакет.

Все эти зависимости можно легко оптимизировать. Мы собрали предложения по оптимизации в репозитории Github,check it out!

Enable module concatenation for ES modules (aka scope hoisting)

Когда вы создаете пакет, webpack превращает каждый модуль в функцию:

// index.js
import {render} from './comments.js';
render();
 
// comments.js
export function render(data, target) {
console.log('Rendered!');
}
 

// bundle.js (part of)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
 
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__comments_js__ = __webpack_require__(1);
Object(__WEBPACK_IMPORTED_MODULE_0__comments_js__["a" /* render */])();
 
}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {
 
"use strict";
__webpack_exports__["a"] = render;
function render(data, target) {
console.log('Rendered!');
}
 
})
 

Ранее это было необходимо для отделения модулей CommonJS/AMD друг от друга. Однако это добавляет объем и проблемы с производительностью.

Webpack 2 представил поддержку модулей ES, в отличие от модулей CommonJS и AMD, вместо того, чтобы оборачивать каждый модуль функцией. Также Webpack 3 используетModuleConcatenationPluginЧтобы завершить такой пакет, следующий пример:

// index.js
import {render} from './comments.js';
render();
 
// comments.js
export function render(data, target) {
console.log('Rendered!');
}
 

// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files
// 与前面的代码不同,这个 bundle 只有一个 module,同时包含两个文件
 
// bundle.js (part of; compiled with ModuleConcatenationPlugin)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
 
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
 
// CONCATENATED MODULE: ./comments.js
function render(data, target) {
console.log('Rendered!');
}
 
// CONCATENATED MODULE: ./index.js
render();
 
})
 

Увидеть разницу? В этом комплекте модулю 0 нужен метод рендеринга модуля 1. использоватьModuleConcatenationPlugin,requireбыл просто заменен на функцию require, а модуль 1 был удален. В этом комплекте меньше модулей, поэтому потери модулей меньше!

Включите эту функцию, чтобы добавить в список плагиновModuleConcatenationPlugin:

// webpack.config.js
const webpack = require('webpack');
 
module.exports = {
plugins: [
new webpack.optimize.ModuleConcatenationPlugin(),
],
};
 

Примечание. Хотите знать, почему эта функция не включена по умолчанию? Конкатенация модулей великолепна,Но это увеличит время компиляции и разбивает горячее обновление модуля.这就是为什么只在生产环境中启用的原因了。

Further reading

Use externalsесли у вас есть код как webpack, так и не-webpack, используйте внешние

У вас может быть большой проект, некоторые из которых можно скомпилировать с помощью webpack, а некоторые — нет. Например, веб-сайт с видео, виджет плеера может быть скомпилирован webpack, но окружающая область страницы может не быть:

video-hosting

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

Если зависимость смонтирована в окне

Если ваш код, не относящийся к веб-пакету, зависит от этих зависимостей, которые представляют собой переменные, смонтированные в окне, вы можете присвоить имя зависимости имени переменной:

// webpack.config.js
module.exports = {
externals: {
'react': 'React',
'react-dom': 'ReactDOM',
},
};
 

С этой конфигурацией webpack не будет упаковыватьreactа такжеreact-domМешок. Вместо этого они будут заменены чем-то вроде этого:

// bundle.js (part of)
(function(module, exports) {
// A module that exports `window.React`. Without `externals`,
// this module would include the whole React bundle
module.exports = React;
}),
(function(module, exports) {
// A module that exports `window.ReactDOM`. Without `externals`,
// this module would include the whole ReactDOM bundle
module.exports = ReactDOM;
})
 

Если зависимости загружаются как пакеты AMD

Это еще более сложно, если ваш код, не относящийся к веб-пакету, не монтирует доступ к зависимостям в окне. Но если код, не являющийся веб-пакетом, использует эти зависимости как пакеты AMD, вы все равно можете избежать повторной загрузки кода дважды.

Как это сделать? Скомпилируйте код веб-пакета в модуль AMD и добавьте его в URL-адреса библиотеки:

// webpack.config.js
module.exports = {
output: { libraryTarget: 'amd' },
 
externals: {
'react': { amd: '/libraries/react.min.js' },
'react-dom': { amd: '/libraries/react-dom.min.js' },
},
};
 

Webpack завернет пакет вdefine()Также сделайте его зависимым от этих URL-адресов:

// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });
 

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

Примечание: webpack просто заменяет теexternalsСсылка на точно совпадающие ключи в объекте. Это означает, что если ваш код написан такimport React from 'react/umd/react.production.min.js', эта библиотека не будет исключена из комплекта. Это потому что - webpack не знаетimport 'react'а такжеimport 'react/umd/react.production.min.js'Это та же библиотека, поэтому она более осторожна.

Further reading

Подведение итогов

  • Minimize your code with the UglifyJsPlugin and loader options
  • Remove the development-only code with the DefinePlugin
  • Use ES modules to enable tree shaking
  • Compress images
  • Apply dependency-specific optimizations
  • Enable module concatenation
  • Use externals if this makes sense for you

Используйте долгосрочное кэширование

авторIvan Akulov

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

Использовать управление версиями пакетов и заголовки кеша Использовать управление версиями пакетов и заголовки кеша

Общее решение для кэширования:

1. Скажите браузеру кэшировать файл на долгое время (скажем, на год)

# Server header
Cache-Control: max-age=31536000
 

Примечание: Если вы не знакомы сCache-ControlЧто вы сделали, вы можете взглянуть на этот замечательный пост в блоге Джейка Арчибальда.on caching best practices

2. Переименовывайте эти файлы, когда они изменяются и требуют принудительной повторной загрузки

<!-- Before the change -->
<script src="./index-v15.js"></script>
 
<!-- After the change -->
<script src="./index-v16.js"></script>
 

Эти методы сообщают браузеру, что нужно загрузить эти JS-файлы и кэшировать их. Браузер будет запрашивать сеть (или в случае аннулирования кеша) только в том случае, если имя файла изменилось.

Используя webpack, вы можете сделать то же самое, но вы можете использовать номер версии, чтобы решить эту проблему, вам нужно знать хеш-значение этого файла. использовать[chunkhash]могуhashЗначение включено в имя файла:

// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.[chunkhash].js',
// → bundle.8e0d62a03.js
},
};
 

Примечание: webpack может генерировать разные хэши, даже если пакет один и тот же — например, вы переименовали файл или перекомпилировали пакет в другой ОС.This is a bug.
Если вам нужно отправить имя файла клиенту, вы также можете использоватьHtmlWebpackPluginилиWebpackManifestPlugin.

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

<!-- index.html -->
<!doctype html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>
 

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

// manifest.json
{
"bundle.js": "bundle.8e0d62a03.js"
}
 

Further reading

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

Зависимости

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

Ключевой термин: в технологии веб-пакетов разделение файлов с помощью кода приложения называетсяchunks. Мы будем использовать этот термин позже.

Чтобы извлечь зависимые пакеты в отдельные фрагменты, следующее разделено на три шага:

1. Используйте[name].[chunkname].jsзаменятьoutputимя файла:

// webpack.config.js
module.exports = {
output: {
// Before
filename: 'bundle.[chunkhash].js',
// After
filename: '[name].[chunkhash].js',
},
};
 

Когда веб-пакет создает приложение, он заменяет имя фрагментом[name]. Если не добавлено[name]часть, нам пришлось сравнивать куски по их разнице в хэшах - это слишком сложно!

2. будетentryПреобразовать в объект:

// webpack.config.js
module.exports = {
// Before
entry: './index.js',
// After
entry: {
main: './index.js',
},
};
 

В этом коде «главным» объектом является имя чанка. Это имя будет использоваться на шаге 1.[name]заменять. До сих пор, если вы создаете приложение, блок включает в себя весь код приложения — точно так же, как мы не делали эти шаги. Но это скоро изменится.

3. ДобавитьCommonsChunkPlugin:

// webpack.config.js
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
// A name of the chunk that will include the dependencies.
// This name is substituted in place of [name] from step 1
name: 'vendor',
 
// A function that determines which modules to include into this chunk
minChunks: module => module.context &&
module.context.includes('node_modules'),
}),
],
};
 

Плагин будет включать всеnode_modulesмодули в пути и переместите их в отдельный файл с именемvendor.[chunkhash].js.

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

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor
 

Webpack runtime code

К сожалению, только извлечениеvendorнедостаточно. Если вы попытаетесь что-то изменить в коде приложения:

// index.js
…
…
 
// E.g. add this:
console.log('Wat');
 

Ты заметишьvendorЗначение хеша также изменится:

Asset Size Chunks Chunk Names
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor
 

Asset Size Chunks Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js 47 kB 1 [emitted] vendor
 

Это происходит из-за того, что при упаковке веб-пакета код некоторых модулейa runtime– Модуль управления выполняет часть кода. Когда вы разбиваете свой код на несколько файлов, этот небольшой фрагмент кода запускает сопоставление между идентификаторами фрагментов и соответствующими файлами:

// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
"0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";
 

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

Чтобы решить эту проблему, мы выносим среду выполнения в отдельный файл черезCommonsChunkPluginСоздайте дополнительный пустой фрагмент:

// webpack.config.js
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
 
minChunks: module => module.context &&
module.context.includes('node_modules'),
}),
 
// This plugin must come after the vendor one (because webpack
// includes runtime into the last chunk)
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',
 
// minChunks: Infinity means that no app modules
// will be included into this chunk
minChunks: Infinity,
}),
],
};
 

После завершения этой части изменения каждая сборка будет генерировать три файла:

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 1 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
 

Добавьте их в обратном порядке к index.html, и все готово:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>
 

Further reading

Встроенная среда выполнения веб-пакета для сохранения дополнительного HTTP-запроса

Чтобы сделать его еще лучше, попробуйте встроить среду выполнения веб-пакета в запрос HTML. Следующий пример:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
 

Сюда:

<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>
 

Время выполнения очень маленькое, встроенное это может помочь вам сохранить HTTP-запросы (особенно важно для HTTP/1; но в HTTP/2 это не менее важно, но все же сможет повысить эффективность).

Вот как это сделать.

Если вы используете HtmlWebpackPlugin для генерации HTML

При использованииHtmlWebpackPluginдля создания файлов HTML,InlineChunkWebpackPluginБудет достаточно.

Если вы используете собственную пользовательскую логику для генерации HTML-сервиса

БудуruntimeИзмените имя на статическое явное имя файла:

// webpack.config.js
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',
minChunks: Infinity,
filename: 'runtime.js',
// → Now the runtime file will be called
// “runtime.js”, not “runtime.79f17c27b335abc7aaf4.js”
}),
],
};
 

2. Встроить runtime.js удобным способом. Например: Node.js и Express

// server.js
const fs = require('fs');
const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8');
 
app.get('/', (req, res) =&gt; {
res.send(`
…
${runtimeContent}
…
`);
});
 

ленивая загрузка

Иногда страница состоит из большего или меньшего количества частей:

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

В этих случаях ленивая загрузка оставшихся областей может улучшить начальную производительность загрузки за счет загрузки только самых важных частей. использоватьthe import() functionа такжеcode-splittingрешить эту проблему:

// videoPlayer.js
export function renderVideoPlayer() { … }
 
// comments.js
export function renderComments() { … }
 
// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();
 
// …Custom event listener
onShowCommentsClick(() => {
import('./comments').then((comments) => {
comments.renderComments();
});
});
 

import()Дайте понять, что вы ожидаете динамической загрузки отдельных модулей. когда вебпак видитimport('./module.js'), он переместит модуль в отдельный чанк:

$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.f7e53d8e13e9a2745d6d.js 60 kB 1 [emitted] main
./vendor.4f14b6326a80f4752a98.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
 

и только когда код выполняется дляimport()Будет скачать.

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

Примечание. Если вы используете Babel для компиляции своего кода, вы еще не будете знать Babel.import()И столкнулся с синтаксической ошибкой. можно использоватьsyntax-dynamic-importисправить эту ошибку.

Further reading

Разделите код на маршруты и страницы

Если ваше приложение имеет несколько маршрутов или страниц, но имеет только один файл JS (один основной фрагмент) в вашем коде, похоже, вы экономите дополнительные байты пропускной способности на запрос. Например, когда пользователь посещает домашнюю страницу вашего сайта:

site-home-page

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

Если мы разделим код на страницы (или маршруты в приложении одной страницы), пользователь будет загружать только код, который имеет к нему смысл. Даже лучше, браузеры также лучше кэшируют код лучше: когда вы меняете код на главной странице, WebPack будет недействителен только подходящим кусочком.

Для одностраничных приложений

Разделите ссылки на страницы путем маршрутизации, используйтеimport()(посмотри«Отложенная загрузка кода, который вам сейчас не нужен»эта часть). Если вы используете фреймворк, уже есть зрелые решения:

Для традиционных многостраничных приложений

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

// webpack.config.js
module.exports = {
entry: {
home: './src/Home/index.js',
article: './src/Article/index.js',
profile: './src/Profile/index.js'
},
};
 

Для каждого файла записи webpack построит отдельное дерево зависимостей и потребует пакет, который будет включать только те модули, которые он использует через запись:

$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./home.91b9ed27366fe7e33d6a.js 18 kB 1 [emitted] home
./article.87a128755b16ac3294fd.js 32 kB 2 [emitted] article
./profile.de945dc02685f6166781.js 24 kB 3 [emitted] profile
./vendor.4f14b6326a80f4752a98.js 46 kB 4 [emitted] vendor
./runtime.318d7b8490a7382bf23b.js 1.45 kB 5 [emitted] runtime
 

Поэтому, если только страница статьи используетLodash,homeа такжеprofileпакет не будет включать lodash, и пользователи не будут загружать эту библиотеку при посещении домашней страницы.

Разделение дерева зависимостей также имеет недостатки. Если используются обе точки входаloadash, пока ты неvendorУдалите зависимость, две точки входа будут содержать два дубликата.lodash. мы можем использоватьCommonsChunkPluginчтобы исправить это - он переместит общие зависимости в отдельный файл:

// webpack.config.js
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
// A name of the chunk that will include the common dependencies
name: 'common',
 
// The plugin will move a module into a common file
// only if it’s included into `minChunks` chunks
// (Note that the plugin analyzes all chunks, not only entries)
minChunks: 2, // 2 is the default value
}),
],
};
 

Не стесняйтесь использоватьminChunksзначение для поиска оптимального варианта. Как правило, вы хотите, чтобы он был как можно меньше, но это увеличивает количество фрагментов. Например, 3 куска,minChunksМожет быть 2, но для 30 чанков может быть и 8 - потому что если поставить 2, то слишком много модулей будет упаковано в общий файл, и файл раздуется.

Further reading

Сделать идентификаторы модулей более стабильными

При компиляции кода webpack присваивает каждому модулю идентификатор. Впоследствии эти идентификаторы будутrequire()ссылка внутри пакета. Вы можете увидеть эти идентификаторы перед путем к модулю в правой части вывода компиляции:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.4e50a16675574df6a9e9.js 60 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
 

[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module
 

По умолчанию эти идентификаторы рассчитываются с помощью счетчиков (например, идентификатор первого модуля равен 0, идентификатор второго модуля равен идентификатору 1 и т. д.). Проблема в том, что когда вы добавляете модуль, он появляется в середине исходного списка модулей, изменяя идентификаторы всех последующих модулей:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.5c82c0f337fcb22672b5.js 22 kB 0 [emitted]
./main.0c8b617dfc40c2827ae3.js 82 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
 

↓ Добавляем новый модуль

[4] ./webPlayer.js 24 kB {1} [built]
 

↓ А теперь посмотрите, что здесь сделано!comments.jsТекущий ID изменился с 4 на 5

[5] ./comments.js 58 kB {0} [built]
 

ads.jsID изменен с 5 на 6

[6] ./ads.js 74 kB {1} [built]
+ 1 hidden module
 

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

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

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.6168aaac8461862eab7a.js 22.5 kB 0 [emitted]
./main.a2e49a279552980e3b91.js 60 kB 1 [emitted] main
./vendor.ff9f7ea865884e6a84c8.js 46 kB 2 [emitted] vendor
./runtime.25f5d0204e4f77fa57a1.js 1.45 kB 3 [emitted] runtime
 

↓ Здесь

[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
+ 1 hidden module
 

С помощью этого метода его ID изменится только в том случае, если вы переименуете и удалите модуль. Новые модули не будут влиять друг на друга по идентификатору модуля.

Чтобы включить этот плагин, добавьте в конфигурациюplugins:

// webpack.config.js
module.exports = {
plugins: [
new webpack.HashedModuleIdsPlugin(),
],
};
 

Further reading

Summing up

  • Cache the bundle and differentiate between them by changing their names
  • Split the bundle into app code, vendor code and runtime
  • Inline the runtime to save an HTTP request
  • Lazy-load non-critical code with import
  • Split code by routes/pages to avoid loading unnecessary stuff

Контролируйте и анализируйте приложение

авторIvan Akulov

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

Это может объяснить некоторую часть этого, чтобы помочь вам понять ваш набор инструментов.

Следите за отслеживанием размера раскладывания упакован

можно использовать при разработкеwebpack-dashboardи командная строкаbundlesizeследить за размером приложения.

webpack-dashboard

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

webpack-dashboard

Эта панель инструментов помогает нам отслеживать большие зависимости — если вы добавите зависимость, вы всегда сразу увидите ее в разделе «Модули»!

Чтобы включить эту функцию, вам необходимо установитьwebpack-dashboardМешок:

npm install webpack-dashboard --save-dev
 

При этом добавляем в настроенные плагины:

// webpack.config.js
const DashboardPlugin = require('webpack-dashboard/plugin');
 
module.exports = {
plugins: [
new DashboardPlugin(),
],
};
 

Или, если вы используете сервер разработки на основе Express, вы можете использоватьcompiler.apply():

compiler.apply(new DashboardPlugin());
 

Поэкспериментируйте с приборной панелью, чтобы узнать, что можно улучшить! Например, прокрутите раздел модулей, чтобы найти слишком большую библиотеку и заменить ее меньшей альтернативой.

bundlesize

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

bundlesize

Конфигурация выглядит следующим образом:

Find out the maximum sizesнайти максимальный объем

1. Проанализируйте приложение, чтобы максимально уменьшить его размер, и выполните сборку производственной среды.
2. Вpackage.jsonувеличить вbundlesizeчасть:

// package.json
{
"bundlesize": [
{
"path": "./dist/*"
}
]
}
 
 

3. Используйтеnpxвоплощать в жизньbundlesize:

npx bundlesize
 

Он распечатает сжатый gzip-том каждого файла:

PASS ./dist/icon256.6168aaac8461862eab7a.png: 10.89KB PASS./dist/icon512.c3e073a4100bd0c28a86.png: 13.1KB PASS./dist/main.0c8b617dfc40c2827ae3.js: 16.28KB PASS./dist/vendor.ff9f7ea865884e6a84c8.js: 31.49KB
 

4. Каждое увеличение громкости на 10-20%, вы получите максимальную громкость. Этот запас в 10-20% позволяет вам разрабатывать ваше приложение как обычно, предупреждая вас, когда оно слишком сильно увеличивается в размере.

Enable bundlesizeвключить размер пакета

5. Установкаbundlesizeзависимости разработки

npm install bundlesize --save-dev
 

6. Вpackage.jsonсерединаbundlesizeраздел, объявляющий конкретное максимальное значение. Для определенных файлов (например, изображений) вы можете установить максимальный размер только для типа файла, а не для каждого файла:

// package.json
{
"bundlesize": [
{
"path": "./dist/*.png",
"maxSize": "16 kB",
},
{
"path": "./dist/main.*.js",
"maxSize": "20 kB",
},
{
"path": "./dist/vendor.*.js",
"maxSize": "35 kB",
}
]
}
 

7. Добавьте npm-скрипт для проверки:

// package.json
{
"scripts": {
"check-size": "bundlesize"
}
}
 

8. Настройте автоматический CI для выполнения при каждом нажатииnpm run check-sizeПроверьте. (Если вы разрабатываете проект на Github, вы можете напрямую использоватьintegrate bundlesize with GitHub. )

Это все! Теперь, если вы запуститеnpm run check-sizeИли нажмите код, и вы увидите, достаточно ли мал выходной файл:

bundlesize-output-success

или следующее не удается

bundlesize-output-failure

Further reading

Проанализируйте, почему пакет такой большой

Вы хотите углубиться в пакет, чтобы увидеть, какие модули занимают сколько места.webpack-bundle-analyzer

(Screen recording from GitHub.com/Webpack-con…)

webpack-bundle-analyzer может сканировать пакеты и создавать визуальное окно, чтобы заглянуть внутрь. Используйте этот инструмент визуализации, чтобы найти слишком большие или ненужные зависимости.

Для использования этого анализатора необходимо установитьwebpack-bundle-analyzerМешок:

npm install webpack-bundle-analyzer --save-dev
 

Добавляем плагины в конфиг:

// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
module.exports = {
plugins: [
new BundleAnalyzerPlugin(),
],
};
 

Запуск рабочей сборки плагина откроет страницу состояния в вашем браузере.

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

Примечание: если вы используетеModuleConcatenationPlugin, он может объединить некоторые модули в выходных данных webpack-bundle-analyzer, что сделает отчет меньше. Если вы используете этот плагин, его необходимо отключить при выполнении анализа.

Вот что нужно искать в отчете:

  • большие зависимостиПочему он такой большой? Существуют ли более мелкие альтернативы (например, Preact вместо React)? использовался весь код (например, Moment.js содержит много локальных переменныхthat are often not used and could be dropped)?
  • Повторяющиеся зависимостиВы видите одну и ту же библиотеку в разных файлах? (использоватьCommonsChunkPluginпереместить их в общий файл) или объединить несколько версий одной и той же библиотеки?
  • Аналогичная зависимостьЕсть ли похожие библиотеки с похожим функционалом? (Напримерmomentа такжеdate-fnsилиlodashа такжеlodash-es) попробуйте объединить в один.

Также см. статью Шона Ларкинаgreat analysis of webpack bundles.

Summing up

  • Use webpack-dashboard and bundlesize to stay tuned of how large your app is
  • Dig into what builds up the size with webpack-bundle-analyzer

Conclusion

В заключение:

  • Избавьтесь от ненужных томовСжатие всего, устранение бесполезного кода и добавление зависимостей — это вопрос осторожности.
  • Разделить код маршрутизациейЗагружайте его только тогда, когда это действительно необходимо, и делайте все остальное.
  • код кэшаНекоторые части приложения обновляются реже, чем другие, разделите их на файлы, чтобы они загружались повторно только при необходимости.
  • размер дорожкииспользоватьwebpack-dashboardа такжеwebpack-bundle-analyzerСледите за своим приложением. Проверяйте производительность вашего приложения каждые несколько месяцев.

Webpack — это не просто инструмент, который поможет вам быстрее создавать приложения. Это также помогает сделать ваше приложениеa Progressive Web App, ваше приложение стало более удобным и оснащено автоматическими инструментами заполнения, такими какLighthouseПредложения даются на основе окружающей среды.

не забудь прочитатьwebpack docs- Он предоставляет много информации об оптимизации.

много практиковатьсяwith the training app!