Как webpack помогает с кэшированием ресурсов
Следующий шаг (после оптимизации размера приложения) , улучшающий время загрузки, — это кэширование. Используйте его, чтобы хранить части приложения на стороне клиента и избегать их повторной загрузки каждый раз.
Используйте версионирование пакетов и заголовки кэша.
Обычно при кэшировании используют следующий подход:
Укажите браузеру кэшировать файл на очень долгое время (например, на год):
# Server header Cache-Control: max-age=31536000Если вы не знакомы с тем, что делает
Cache-Control, ознакомьтесь с отличной статьей Джейка Арчибальда о лучших практиках кэширования .и переименовывать файл при его изменении, чтобы принудительно загрузить его заново:
<!-- Before the change --> <script src="./index-v15.js"></script> <!-- After the change --> <script src="./index-v16.js"></script>
Этот подход указывает браузеру загрузить JS-файл, кэшировать его и использовать кэшированную копию. Браузер будет обращаться к сети только в том случае, если имя файла изменится (или если пройдет год).
В webpack вы делаете то же самое, но вместо номера версии указываете хеш файла. Чтобы включить хеш в имя файла, используйте [chunkhash] :
// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.[chunkhash].js' // → bundle.8e0d62a03.js
}
};
Если вам необходимо указать имя файла для отправки клиенту, используйте либо HtmlWebpackPlugin , либо WebpackManifestPlugin .
Плагин HtmlWebpackPlugin — это простой, но менее гибкий подход. Во время компиляции этот плагин генерирует HTML-файл, который включает все скомпилированные ресурсы. Если логика вашего сервера несложная, то этого должно быть достаточно:
<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>
Плагин WebpackManifestPlugin предлагает более гибкий подход, полезный при сложной серверной части. В процессе сборки он генерирует JSON-файл с сопоставлением имен файлов без хеша и имен файлов с хешем. Используйте этот JSON на сервере, чтобы определить, с каким файлом работать:
// manifest.json
{
"bundle.js": "bundle.8e0d62a03.js"
}
Дополнительная информация
- Джейк Арчибальд о передовых методах кэширования
Вынесите зависимости и среду выполнения в отдельный файл.
Зависимости
Зависимости приложения, как правило, изменяются реже, чем сам код приложения. Если вы переместите их в отдельный файл, браузер сможет кэшировать их отдельно и не будет повторно загружать каждый раз при изменении кода приложения.
Для выделения зависимостей в отдельный блок выполните три шага:
Замените имя выходного файла на
[name].[chunkname].js:// webpack.config.js module.exports = { output: { // Before filename: 'bundle.[chunkhash].js', // After filename: '[name].[chunkhash].js' } };При сборке приложения webpack заменяет
[name]именем фрагмента кода. Если мы не добавим часть[name], нам придётся различать фрагменты по их хешу, что довольно сложно!Преобразуйте поле
entryв объект:// webpack.config.js module.exports = { // Before entry: './index.js', // After entry: { main: './index.js' } };В этом фрагменте "main" — это имя блока кода. Это имя будет подставлено вместо
[name]из шага 1.К этому моменту, если вы соберете приложение, этот фрагмент будет включать весь код приложения — как будто мы не выполнили эти шаги. Но через секунду это изменится.
В webpack 4 добавьте в конфигурацию webpack параметр
optimization.splitChunks.chunks: 'all':// webpack.config.js (for webpack 4) module.exports = { optimization: { splitChunks: { chunks: 'all' } } };Эта опция включает интеллектуальное разделение кода. С её помощью webpack будет извлекать код сторонних разработчиков, если его размер превышает 30 КБ (до минификации и сжатия gzip). Он также будет извлекать общий код — это полезно, если ваша сборка создает несколько пакетов (например , если вы разделили ваше приложение на маршруты ).
В webpack 3 добавьте
CommonsChunkPlugin:// webpack.config.js (for webpack 3) 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.
После этих изменений каждая сборка будет генерировать два файла вместо одного: main.[chunkhash].js и vendor.[chunkhash].js ( vendors~main.[chunkhash].js для webpack 4). В случае webpack 4 пакет vendor может не генерироваться, если зависимости невелики — и это нормально:
$ 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
К сожалению, извлечения только кода поставщика недостаточно. Если вы попытаетесь что-либо изменить в коде приложения:
// 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
Это происходит потому, что пакет webpack, помимо кода модулей, содержит среду выполнения — небольшой фрагмент кода, управляющий выполнением модуля. Когда вы разбиваете код на несколько файлов, этот фрагмент кода начинает включать сопоставление идентификаторов фрагментов с соответствующими файлами:
// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
"0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";
Webpack включает эту среду выполнения в последний сгенерированный фрагмент кода, который в нашем случае является vendor . И каждый раз, когда изменяется какой-либо фрагмент кода, этот фрагмент также изменяется, что приводит к изменению всего фрагмента vendor .
Для решения этой проблемы перенесём среду выполнения в отдельный файл. В webpack 4 это достигается включением параметра optimization.runtimeChunk :
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
runtimeChunk: true
}
};
В webpack 3 это можно сделать, создав дополнительный пустой фрагмент кода с помощью CommonsChunkPlugin :
// webpack.config.js (for webpack 3)
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>
Дополнительная информация
- Руководство Webpack по долговременному кэшированию
- Документация Webpack о среде выполнения и манифесте Webpack.
- «Как максимально эффективно использовать плагин CommonsChunkPlugin»
- Как работают
optimization.splitChunksиoptimization.runtimeChunk
Встраивание среды выполнения webpack позволяет избежать лишнего HTTP-запроса.
Чтобы еще больше улучшить ситуацию, попробуйте встроить среду выполнения webpack непосредственно в 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, но всё же может повлиять на результат).
Вот как это сделать.
Если вы генерируете HTML с помощью HtmlWebpackPlugin
Если вы используете HtmlWebpackPlugin для генерации HTML-файла, то InlineSourcePlugin — это всё, что вам нужно:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
inlineSource: 'runtime~.+\\.js',
}),
new InlineSourcePlugin()
]
};
Если вы генерируете HTML, используя собственную серверную логику
С webpack 4:
Добавьте плагин
WebpackManifestPlugin, чтобы узнать сгенерированное имя фрагмента среды выполнения:// webpack.config.js (for webpack 4) const ManifestPlugin = require('webpack-manifest-plugin'); module.exports = { plugins: [ new ManifestPlugin() ] };При сборке с использованием этого плагина будет создан файл следующего вида:
// manifest.json { "runtime~main.js": "runtime~main.8e0d62a03.js" }Встраивайте содержимое исполняемого блока удобным способом. Например, с помощью Node.js и Express:
// server.js const fs = require('fs'); const manifest = require('./manifest.json'); const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8'); app.get('/', (req, res) => { res.send(` … <script>${runtimeContent}</script> … `); });
Или с использованием webpack 3:
Сделайте имя среды выполнения статическим, указав
filename:module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, filename: 'runtime.js' }) ] };Встраивайте содержимое файла
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(` … <script>${runtimeContent}</script> … `); });
Ленивая загрузка кода, который вам сейчас не нужен.
Иногда страница содержит более и менее важные части:
- Если вы открываете страницу с видео на YouTube, вас больше интересует само видео, чем комментарии. Здесь же видео важнее комментариев.
- Если вы открываете статью на новостном сайте, вас больше интересует текст статьи, чем реклама. Здесь текст важнее рекламы.
В таких случаях улучшите производительность первоначальной загрузки, сначала загружая только самые важные файлы, а оставшиеся части загружая с задержкой. Для этого используйте функцию import() и разделение кода :
// 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() указывает, что вы хотите динамически загрузить определенный модуль. Когда webpack видит 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() .
Это уменьшит размер main пакета, улучшив время первоначальной загрузки. Более того, это улучшит кэширование — если вы измените код в основном блоке, это не повлияет на блок комментариев.
Дополнительная информация
- Документация Webpack по функции
import() - Предложение по реализации синтаксиса
import()в JavaScript
Разделите код на маршруты и страницы.
Если в вашем приложении несколько маршрутов или страниц, но код содержится только в одном JS-файле (одном main фрагменте), вероятно, вы передаёте лишние байты при каждом запросе. Например, когда пользователь посещает главную страницу вашего сайта:

Им не нужно загружать код для отображения статьи, которая находится на другой странице, — но они его загрузят. Более того, если пользователь всегда посещает только главную страницу, и вы вносите изменения в код статьи, webpack аннулирует весь пакет, и пользователю придется заново скачивать все приложение.
Если мы разделим приложение на страницы (или маршруты, если это одностраничное приложение), пользователь загрузит только необходимый код. Кроме того, браузер будет лучше кэшировать код приложения: если вы измените код главной страницы, webpack аннулирует только соответствующий фрагмент.
Для одностраничных приложений
Для разделения одностраничных приложений по маршрутам используйте import() (см. раздел «Ленивая загрузка кода, который вам сейчас не нужен» ). Если вы используете фреймворк, возможно, для этого уже есть готовое решение:
- "Разделение кода" в документации
react-router(для React) - «Ленивая загрузка маршрутов» в документации
vue-router(для Vue.js)
Для традиционных многостраничных приложений
Для разделения традиционных приложений на страницы используйте точки входа webpack. Если ваше приложение имеет три типа страниц: главную страницу, страницу статьи и страницу учетной записи пользователя, — оно должно иметь три точки входа:
// 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, и вы не перенесли свои зависимости в пакет поставщика, обе точки входа будут включать копию Lodash. Чтобы решить эту проблему, в webpack 4 добавьте опцию optimization.splitChunks.chunks: 'all' в конфигурацию webpack:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
Эта опция включает интеллектуальное разделение кода. При её использовании webpack автоматически будет искать общий код и выносить его в отдельные файлы.
Или, в webpack 3, используйте CommonsChunkPlugin — он переместит общие зависимости в новый указанный файл:
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
minChunks: 2 // 2 is the default value
})
]
};
Не стесняйтесь экспериментировать со значением minChunks , чтобы найти оптимальное. Как правило, лучше всего оставлять его небольшим, но увеличивать по мере роста количества блоков. Например, для 3 блоков minChunks может быть равно 2, а для 30 блоков — 8, потому что если оставить его равным 2, слишком много модулей попадут в общий файл, что приведет к его чрезмерному раздуванию.
Дополнительная информация
- Документация Webpack о концепции точек входа
- Документация Webpack о плагине CommonsChunkPlugin
- «Как максимально эффективно использовать плагин CommonsChunkPlugin»
- Как работают
optimization.splitChunksиoptimization.runtimeChunk
Сделать идентификаторы модулей более стабильными.
При сборке кода webpack присваивает каждому модулю идентификатор (ID). Позже эти идентификаторы используются в 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
По умолчанию идентификаторы вычисляются с помощью счетчика (например, первый модуль имеет ID 0, второй — ID 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 5 вместо 4.
[5] ./comments.js 58 kB {0} [built]
↓ ads.js теперь используется ID 6 вместо 5.
[6] ./ads.js 74 kB {1} [built]
+ 1 hidden module
Это делает недействительными все фрагменты кода, которые включают модули с измененными идентификаторами или зависят от них, — даже если их фактический код не изменился. В нашем случае недействительными становятся фрагмент 0 (фрагмент с comments.js ) и 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
При таком подходе идентификатор модуля изменяется только в случае его переименования или перемещения. Новые модули не повлияют на идентификаторы других модулей.
Чтобы включить плагин, добавьте его в раздел plugins файла конфигурации:
// webpack.config.js
module.exports = {
plugins: [
new webpack.HashedModuleIdsPlugin()
]
};
Дополнительная информация
- Документация Webpack о плагине HashedModuleIdsPlugin
Подводя итог
- Кэшируйте пакет и различайте версии, изменяя имя пакета.
- Разделите пакет на код приложения, код поставщика и среду выполнения.
- Встройте среду выполнения, чтобы сократить объем HTTP-запроса.
- Ленивая загрузка некритического кода с помощью
import - Разделите код по маршрутам/страницам, чтобы избежать загрузки ненужных элементов.