оригинальныйDevelopers.Google.com/Web/Женщины большие…
авторAddy Osmani,Ivan Akulov
Введение
авторAddy Osmani
Современные веб-приложения часто используютbunding tool
Чтобы создавать упакованные файлы (например, сценарии, стили и т. д.) для производственных сред, упакованные файлы необходимо оптимизировать и сжать до минимума, чтобы пользователи могли загружать их быстрее. В этой статье мы будем использоватьwebpack
чтобы объяснить, как оптимизировать ресурсы веб-сайта во всем. Это поможет пользователям быстрее загрузить ваше приложение и улучшить его работу.
В настоящее время Webpack является одним из самых популярных инструментов для создания пакетов. Использование его функций для оптимизации кода, разделения сценариев на важные и второстепенные части и устранения бесполезного кода может гарантировать, что ваше приложение поддерживает минимальную пропускную способность и потребление процессов.
Примечание. Мы создали приложение для упражнений, чтобы продемонстрировать приведенные ниже описания этих оптимизаций. Постарайтесь как можно больше времени практиковать эти советы.
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 } }, ], }, ], }, }; ``` > 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
- Что такое «переменные среды»
- Webpack docs about:
DefinePlugin
,EnvironmentPlugin
Используйте модули 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
orbabel-preset-es2015
, чтобы проверить текущую конфигурацию. По умолчанию ЕСimport
andexport
to CommonJSrequire
andmodule.exports
. установив возможность отключитьPass the{ modules: false }
option.
Futher reading
- "Модули ES6 в деталях"
- Webpack docs about tree shaking
Оптимизация изображений
Картинка в основном будет занимать больше половины объема страницы. Хотя они не так важны, как 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
- Webpack docs for the ModuleConcatenationPlugin
- «Краткое введение в подъем прицела»
- Detailed description of what this plugin does
Use externals
если у вас есть код как webpack, так и не-webpack, используйте внешние
У вас может быть большой проект, некоторые из которых можно скомпилировать с помощью webpack, а некоторые — нет. Например, веб-сайт с видео, виджет плеера может быть скомпилирован webpack, но окружающая область страницы может не быть:
Если два фрагмента кода имеют одинаковые зависимости, вы можете поделиться этими зависимостями, чтобы сократить время повторной загрузки.веб-пакет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
- Webpack docs on
externals
Подведение итогов
- 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
- Jake Archibald about caching best practices
Извлечь зависимости и среду выполнения в отдельный файл Извлечь зависимости и код среды выполнения в отдельный файл
Зависимости
Зависимости приложения обычно имеют меньше изменений, чем фактический код приложения. Если вы переместите их в отдельные файлы, браузер сможет кэшировать их самостоятельно — без повторной загрузки при каждом изменении кода приложения.
Ключевой термин: в технологии веб-пакетов разделение файлов с помощью кода приложения называется
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
- Webpack guide on long term caching
- Webpack docs about webpack runtime and manifest
- «Получить максимальную отдачу от CommonsChunkPlugin»
Встроенная среда выполнения веб-пакета для сохранения дополнительного 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) => {
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
- Webpack docs for the
import()
function - The JavaScript proposal for implementing the
import()
syntax
Разделите код на маршруты и страницы
Если ваше приложение имеет несколько маршрутов или страниц, но имеет только один файл JS (один основной фрагмент) в вашем коде, похоже, вы экономите дополнительные байты пропускной способности на запрос. Например, когда пользователь посещает домашнюю страницу вашего сайта:
Им не нужно загружать дополнительную отрисовку заголовка статьи на другой код страницы — но они будут загружены в код. А если серьезно, если пользователь часто посещает только домашнюю страницу, а вы часто меняете код для отображения заголовка статьи, весь пакет веб-пакета не удастся — он будет повторяться каждый раз, когда пользователь загружает весь код приложения.
Если мы разделим код на страницы (или маршруты в приложении одной страницы), пользователь будет загружать только код, который имеет к нему смысл. Даже лучше, браузеры также лучше кэшируют код лучше: когда вы меняете код на главной странице, WebPack будет недействителен только подходящим кусочком.
Для одностраничных приложений
Разделите ссылки на страницы путем маршрутизации, используйтеimport()
(посмотри«Отложенная загрузка кода, который вам сейчас не нужен»эта часть). Если вы используете фреймворк, уже есть зрелые решения:
-
«Разделение кода» in
react-router
документы (для React) -
«Отложенная загрузка маршрутов» in
vue-router
документы (для Vue.js)
Для традиционных многостраничных приложений
Разделяя традиционные многостраничные приложения по страницам, вы можете использовать веб-пакеты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 docs about the concept of entry points
- Webpack docs about the CommonsChunkPlugin
- «Получить максимальную отдачу от CommonsChunkPlugin»
Сделать идентификаторы модулей более стабильными
При компиляции кода 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.js
ID изменен с 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
- Webpack docs about the HashedModuleIdsPlugin
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Мешок:
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Можно проверить, что ресурсы веб-пакета не превышают указанный размер. Автоматизируйте непрерывную интеграцию, чтобы узнать, не слишком ли раздувается ваше приложение:
Конфигурация выглядит следующим образом:
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
Или нажмите код, и вы увидите, достаточно ли мал выходной файл:
или следующее не удается
Further reading
- Alex Russell about the real-world loading time we should target
Проанализируйте, почему пакет такой большой
Вы хотите углубиться в пакет, чтобы увидеть, какие модули занимают сколько места.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
andbundlesize
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!