Как веб-пакет помогает с кэшированием ресурсов
Следующее (после оптимизации размера приложения , которое сокращает время загрузки приложения) — это кеширование. Используйте его, чтобы хранить части приложения на клиенте и избегать их повторной загрузки каждый раз.
Используйте управление версиями пакетов и заголовки кэша.
Общий подход к кэшированию заключается в следующем:
сообщите браузеру кэшировать файл на очень долгое время (например, год):
# 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, кэшировать его и использовать кэшированную копию. Браузер попадет в сеть только в том случае, если имя файла изменится (или пройдет год).
С вебпаком вы делаете то же самое, но вместо номера версии указываете хеш файла. Чтобы включить хэш в имя файла, используйте [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' } };
Когда веб-пакет собирает приложение, он заменяет
[name]
именем чанка. Если мы не добавим часть[name]
, нам придется различать чанки по их хешу – а это довольно сложно!Преобразуйте поле
entry
в объект:// webpack.config.js module.exports = { // Before entry: './index.js', // After entry: { main: './index.js' } };
В этом фрагменте «main» — это имя чанка. Это имя будет заменено на
[name]
из шага 1.К этому моменту, если вы создадите приложение, этот фрагмент будет включать в себя весь код приложения – точно так же, как мы не делали этих шагов. Но это изменится через секунду.
В веб-пакете 4 добавьте
optimization.splitChunks.chunks: 'all'
в конфигурацию вашего веб-пакета:// webpack.config.js (for webpack 4) module.exports = { optimization: { splitChunks: { chunks: 'all' } } };
Эта опция включает интеллектуальное разделение кода. С его помощью веб-пакет будет извлекать код поставщика, если его размер превышает 30 КБ (до минификации и gzip). Также будет извлечен общий код — это полезно, если ваша сборка создает несколько пакетов (например , если вы разделите свое приложение на маршруты ).
В веб-пакет 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). В случае с веб-пакетом 4 пакет поставщика может не сгенерироваться, если зависимости небольшие — и это нормально:
$ 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
Браузер будет кэшировать эти файлы отдельно и повторно загружать только изменяющийся код.
Код времени выполнения веб-пакета
К сожалению, извлечь только код поставщика недостаточно. Если вы попытаетесь изменить что-то в коде приложения:
// 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
Это происходит потому, что в комплекте веб-пакета, кроме кода модулей, есть среда выполнения — небольшой фрагмент кода, который управляет выполнением модуля. Когда вы разделяете код на несколько файлов, этот фрагмент кода начинает включать сопоставление между идентификаторами фрагментов и соответствующими файлами:
// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
"0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";
Webpack включает эту среду выполнения в последний сгенерированный чанк, который в нашем случае vendor
. И каждый раз, когда изменяется какой-либо фрагмент, этот фрагмент кода тоже меняется, вызывая изменение всего фрагмента vendor
.
Чтобы решить эту проблему, давайте переместим среду выполнения в отдельный файл. В веб-пакете 4 это достигается включением опции optimization.runtimeChunk
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
runtimeChunk: true
}
};
В веб-пакете 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 о среде выполнения и манифесте веб-пакета
- «Как получить максимальную отдачу от плагина CommonsChunkPlugin»
- Как работают
optimization.splitChunks
иoptimization.runtimeChunk
Встроенная среда выполнения веб-пакета для сохранения дополнительного 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, но все равно может иметь эффект).
Вот как это сделать.
Если вы генерируете 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 с использованием пользовательской логики сервера
С веб-пакетом 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> … `); });
Или с веб-пакетом 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()
указывает, что вы хотите динамически загружать определенный модуль. Когда веб-пакет видит 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()
- Предложение JavaScript по реализации синтаксиса
import()
Разделите код на маршруты и страницы
Если ваше приложение имеет несколько маршрутов или страниц, но есть только один JS-файл с кодом (один main
фрагмент), вполне вероятно, что вы обслуживаете дополнительные байты при каждом запросе. Например, когда пользователь посещает домашнюю страницу вашего сайта:
им не нужно загружать код для рендеринга статьи, находящейся на другой странице, но они его загрузят. Более того, если пользователь всегда посещает только домашнюю страницу, а вы вносите изменения в код статьи, вебпак аннулирует весь пакет — и пользователю придется заново загружать все приложение.
Если мы разделим приложение на страницы (или маршруты, если это одностраничное приложение), пользователь загрузит только соответствующий код. Кроме того, браузер лучше кэширует код приложения: если вы измените код домашней страницы, веб-пакет аннулирует только соответствующий фрагмент.
Для одностраничных приложений
Чтобы разделить одностраничные приложения по маршрутам, используйте import()
(см. раздел «Код отложенной загрузки, который вам сейчас не нужен» ). Если вы используете фреймворк, у него может быть существующее решение для этого:
- «Разделение кода» в документации
react-router
(для React) - «Маршруты отложенной загрузки» в документации
vue-router
(для Vue.js)
Для традиционных многостраничных приложений
Чтобы разделить традиционные приложения по страницам, используйте точки входа веб-пакета. Если в вашем приложении есть три типа страниц: домашняя страница, страница статьи и страница учетной записи пользователя, в нем должно быть три записи:
// webpack.config.js
module.exports = {
entry: {
home: './src/Home/index.js',
article: './src/Article/index.js',
profile: './src/Profile/index.js'
}
};
Для каждого файла записи веб-пакет построит отдельное дерево зависимостей и сгенерирует пакет, включающий только модули, используемые этой записью:
$ 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. Чтобы решить эту проблему, в веб-пакете 4 добавьте параметр optimization.splitChunks.chunks: 'all'
в конфигурацию вашего веб-пакета:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
Эта опция включает интеллектуальное разделение кода. При использовании этой опции веб-пакет будет автоматически искать общий код и извлекать его в отдельные файлы.
Или в веб-пакете 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
Сделать идентификаторы модулей более стабильными
При создании кода веб-пакет присваивает каждому модулю идентификатор. Позже эти идентификаторы используются в 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
теперь имеет идентификатор 5 вместо 4
[5] ./comments.js 58 kB {0} [built]
↓ ads.js
теперь имеет идентификатор 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
- Разделите код по маршрутам/страницам, чтобы избежать загрузки ненужного материала.
Как веб-пакет помогает с кэшированием ресурсов
Следующее (после оптимизации размера приложения , которое сокращает время загрузки приложения) — это кеширование. Используйте его, чтобы хранить части приложения на клиенте и избегать их повторной загрузки каждый раз.
Используйте управление версиями пакетов и заголовки кэша.
Общий подход к кэшированию заключается в следующем:
сообщите браузеру кэшировать файл на очень долгое время (например, год):
# 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, кэшировать его и использовать кэшированную копию. Браузер попадет в сеть только в том случае, если имя файла изменится (или пройдет год).
С вебпаком вы делаете то же самое, но вместо номера версии указываете хеш файла. Чтобы включить хэш в имя файла, используйте [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' } };
Когда веб-пакет собирает приложение, он заменяет
[name]
именем чанка. Если мы не добавим часть[name]
, нам придется различать чанки по их хешу – а это довольно сложно!Преобразуйте поле
entry
в объект:// webpack.config.js module.exports = { // Before entry: './index.js', // After entry: { main: './index.js' } };
В этом фрагменте «main» — это имя чанка. Это имя будет заменено на
[name]
из шага 1.К этому моменту, если вы создадите приложение, этот фрагмент будет включать в себя весь код приложения – точно так же, как мы не делали этих шагов. Но это изменится через секунду.
В веб-пакете 4 добавьте
optimization.splitChunks.chunks: 'all'
в конфигурацию вашего веб-пакета:// webpack.config.js (for webpack 4) module.exports = { optimization: { splitChunks: { chunks: 'all' } } };
Эта опция включает интеллектуальное разделение кода. С его помощью веб-пакет будет извлекать код поставщика, если его размер превышает 30 КБ (до минификации и gzip). Также будет извлечен общий код — это полезно, если ваша сборка создает несколько пакетов (например , если вы разделите свое приложение на маршруты ).
В веб-пакет 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). В случае с веб-пакетом 4 пакет поставщика может не сгенерироваться, если зависимости небольшие — и это нормально:
$ 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
Браузер будет кэшировать эти файлы отдельно и повторно загружать только изменяющийся код.
Код времени выполнения веб-пакета
К сожалению, извлечь только код поставщика недостаточно. Если вы попытаетесь изменить что-то в коде приложения:
// 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
Это происходит потому, что в комплекте веб-пакета, кроме кода модулей, есть среда выполнения — небольшой фрагмент кода, который управляет выполнением модуля. Когда вы разделяете код на несколько файлов, этот фрагмент кода начинает включать сопоставление между идентификаторами фрагментов и соответствующими файлами:
// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
"0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";
Webpack включает эту среду выполнения в последний сгенерированный чанк, который в нашем случае vendor
. И каждый раз, когда изменяется какой-либо фрагмент, этот фрагмент кода тоже меняется, вызывая изменение всего фрагмента vendor
.
Чтобы решить эту проблему, давайте переместим среду выполнения в отдельный файл. В веб-пакете 4 это достигается включением опции optimization.runtimeChunk
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
runtimeChunk: true
}
};
В веб-пакете 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 о среде выполнения и манифесте веб-пакета
- «Как получить максимальную отдачу от плагина CommonsChunkPlugin»
- Как работают
optimization.splitChunks
иoptimization.runtimeChunk
Встроенная среда выполнения веб-пакета для сохранения дополнительного 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, но все равно может иметь эффект).
Вот как это сделать.
Если вы генерируете 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 с использованием пользовательской логики сервера
С веб-пакетом 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> … `); });
Или с веб-пакетом 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()
указывает, что вы хотите динамически загружать определенный модуль. Когда веб-пакет видит 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()
- Предложение JavaScript по реализации синтаксиса
import()
Разделите код на маршруты и страницы
Если ваше приложение имеет несколько маршрутов или страниц, но есть только один JS-файл с кодом (один main
фрагмент), вполне вероятно, что вы обслуживаете дополнительные байты при каждом запросе. Например, когда пользователь посещает домашнюю страницу вашего сайта:
им не нужно загружать код для рендеринга статьи, находящейся на другой странице, но они его загрузят. Более того, если пользователь всегда посещает только домашнюю страницу, а вы вносите изменения в код статьи, вебпак аннулирует весь пакет — и пользователю придется заново загружать все приложение.
Если мы разделим приложение на страницы (или маршруты, если это одностраничное приложение), пользователь загрузит только соответствующий код. Кроме того, браузер лучше кэширует код приложения: если вы измените код домашней страницы, веб-пакет сделает недействительным только соответствующий фрагмент.
Для одностраничных приложений
Чтобы разделить одностраничные приложения по маршрутам, используйте import()
(см. раздел «Код отложенной загрузки, который вам сейчас не нужен» ). Если вы используете фреймворк, у него может быть существующее решение для этого:
- «Разделение кода» в документации
react-router
(для React) - «Маршруты отложенной загрузки» в документации
vue-router
(для Vue.js)
Для традиционных многостраничных приложений
Чтобы разделить традиционные приложения по страницам, используйте точки входа веб-пакета. Если в вашем приложении есть три типа страниц: домашняя страница, страница статьи и страница учетной записи пользователя, в нем должно быть три записи:
// webpack.config.js
module.exports = {
entry: {
home: './src/Home/index.js',
article: './src/Article/index.js',
profile: './src/Profile/index.js'
}
};
Для каждого файла записи веб-пакет построит отдельное дерево зависимостей и сгенерирует пакет, включающий только модули, используемые этой записью:
$ 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. Чтобы решить эту проблему, в веб-пакете 4 добавьте параметр optimization.splitChunks.chunks: 'all'
в конфигурацию вашего веб-пакета:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
Эта опция включает интеллектуальное разделение кода. При использовании этой опции веб-пакет будет автоматически искать общий код и извлекать его в отдельные файлы.
Или в веб-пакете 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
Сделать идентификаторы модулей более стабильными
При создании кода веб-пакет присваивает каждому модулю идентификатор. Позже эти идентификаторы используются в 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
теперь имеет идентификатор 5 вместо 4
[5] ./comments.js 58 kB {0} [built]
↓ ads.js
теперь имеет идентификатор 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
- Разделите код по маршрутам/страницам, чтобы избежать загрузки ненужного материала.
Как веб-пакет помогает с кэшированием ресурсов
Следующее (после оптимизации размера приложения , которое сокращает время загрузки приложения) — это кеширование. Используйте его, чтобы хранить части приложения на клиенте и избегать их повторной загрузки каждый раз.
Используйте управление версиями пакетов и заголовки кэша.
Общий подход к кэшированию заключается в следующем:
сообщите браузеру кэшировать файл на очень долгое время (например, год):
# 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, кэшировать его и использовать кэшированную копию. Браузер попадет в сеть только в том случае, если имя файла изменится (или пройдет год).
С вебпаком вы делаете то же самое, но вместо номера версии указываете хеш файла. Чтобы включить хэш в имя файла, используйте [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' } };
Когда веб-пакет собирает приложение, он заменяет
[name]
именем чанка. Если мы не добавим часть[name]
, нам придется различать чанки по их хешу – а это довольно сложно!Преобразуйте поле
entry
в объект:// webpack.config.js module.exports = { // Before entry: './index.js', // After entry: { main: './index.js' } };
В этом фрагменте «main» — это имя чанка. Это имя будет заменено на
[name]
из шага 1.К этому моменту, если вы создадите приложение, этот фрагмент будет включать в себя весь код приложения – точно так же, как мы не делали этих шагов. Но это изменится через секунду.
В веб-пакете 4 добавьте
optimization.splitChunks.chunks: 'all'
в конфигурацию вашего веб-пакета:// webpack.config.js (for webpack 4) module.exports = { optimization: { splitChunks: { chunks: 'all' } } };
Эта опция включает интеллектуальное разделение кода. С его помощью веб-пакет будет извлекать код поставщика, если его размер превышает 30 КБ (до минификации и gzip). Также будет извлечен общий код — это полезно, если ваша сборка создает несколько пакетов (например , если вы разделите свое приложение на маршруты ).
В веб-пакет 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). В случае с веб-пакетом 4 пакет поставщика может не сгенерироваться, если зависимости небольшие — и это нормально:
$ 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
Браузер будет кэшировать эти файлы отдельно и повторно загружать только изменяющийся код.
Код времени выполнения веб-пакета
К сожалению, извлечь только код поставщика недостаточно. Если вы попытаетесь что-то изменить в коде приложения:
// 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
Это происходит потому, что в комплекте веб-пакета, кроме кода модулей, есть среда выполнения — небольшой фрагмент кода, который управляет выполнением модуля. Когда вы разделяете код на несколько файлов, этот фрагмент кода начинает включать сопоставление между идентификаторами фрагментов и соответствующими файлами:
// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
"0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";
Webpack включает эту среду выполнения в последний сгенерированный чанк, который в нашем случае vendor
. И каждый раз, когда меняется какой-либо фрагмент, этот фрагмент кода тоже меняется, вызывая изменение всего фрагмента vendor
.
Чтобы решить эту проблему, давайте переместим среду выполнения в отдельный файл. В веб-пакете 4 это достигается включением опции optimization.runtimeChunk
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
runtimeChunk: true
}
};
В веб-пакете 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 о среде выполнения и манифесте веб-пакета
- «Как получить максимальную отдачу от CommonsChunkPlugin»
- Как работают
optimization.splitChunks
иoptimization.runtimeChunk
Встроенная среда выполнения веб-пакета для сохранения дополнительного 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, но все равно может иметь эффект).
Вот как это сделать.
Если вы генерируете 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 с использованием пользовательской логики сервера
С веб-пакетом 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> … `); });
Или с веб-пакетом 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()
указывает, что вы хотите динамически загружать определенный модуль. Когда веб-пакет видит 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()
- Предложение JavaScript по реализации синтаксиса
import()
Разделите код на маршруты и страницы
Если ваше приложение имеет несколько маршрутов или страниц, но есть только один JS-файл с кодом (один main
фрагмент), вполне вероятно, что вы обслуживаете дополнительные байты при каждом запросе. Например, когда пользователь посещает домашнюю страницу вашего сайта:
им не нужно загружать код для рендеринга статьи, находящейся на другой странице, но они его загрузят. Более того, если пользователь всегда посещает только домашнюю страницу, а вы вносите изменения в код статьи, вебпак аннулирует весь пакет — и пользователю придется заново загружать все приложение.
Если мы разделим приложение на страницы (или маршруты, если это одностраничное приложение), пользователь загрузит только соответствующий код. Кроме того, браузер будет лучше кэшировать код приложения: если вы измените код домашней страницы, 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 Docs о концепции точек входа
- WebPack Docs о CommonsChunkPlugin
- "Получение максимальной отдачи от Commonschunkplugin"
- Как
optimization.splitChunks
иoptimization.runtimeChunk
Сделайте идентификаторы модуля более стабильными
При создании кода WebPack назначает каждому модулю идентификатор. Позже эти идентификаторы используются в require()
s внутри пакета. Вы обычно видите идентификаторы на выходе сборки прямо перед путями модуля:
$ 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 Docs о Hashedmoduleidsplugin
Суммирование
- Кэшируйте пакет и дифференцируйте версии, изменяя имя пакета
- Разделите пакет на код приложения, код поставщика и время выполнения.
- Встроить время выполнения, чтобы сохранить HTTP -запрос
- Некритический код с ленивой нагрузкой с
import
- Разделенный код по маршрутам/страницам, чтобы избежать загрузки ненужных вещей
Как 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 добавьте опцию
optimization.splitChunks.chunks: 'all'
в конфигурации WebPack:// 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
vendors~main.[chunkhash].js
vendor.[chunkhash].js
В случае WebPack 4, пакет продавцов может не генерироваться, если зависимости невелики - и это нормально:
$ 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
Браузер будет кэшировать эти файлы отдельно - и Redownload только код, который меняется.
WebPack Code
К сожалению, извлечения только кода поставщика недостаточно. Если вы попытаетесь что -то изменить в коде приложения:
// 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 Docs о WebPack Stime и Manifest
- "Получение максимальной отдачи от 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
пакет меньше, улучшая начальное время загрузки. Более того, это улучшит кэширование - если вы измените код в основной части, комментарии Chunk не повлияет.
Дальнейшее чтение
- WebPack Docs для функции
import()
- Предложение JavaScript для реализации синтаксиса
import()
Разделите код на маршруты и страницы
Если в вашем приложении есть несколько маршрутов или страниц, но есть только один файл 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 Docs о концепции точек входа
- WebPack Docs о CommonsChunkPlugin
- "Получение максимальной отдачи от Commonschunkplugin"
- Как
optimization.splitChunks
иoptimization.runtimeChunk
Сделайте идентификаторы модуля более стабильными
При создании кода WebPack назначает каждому модулю идентификатор. Позже эти идентификаторы используются в require()
s внутри пакета. Вы обычно видите идентификаторы на выходе сборки прямо перед путями модуля:
$ 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 Docs о Hashedmoduleidsplugin
Суммирование
- Кэшируйте пакет и дифференцируйте версии, изменяя имя пакета
- Разделите пакет на код приложения, код поставщика и время выполнения.
- Встроить время выполнения, чтобы сохранить HTTP -запрос
- Некритический код с ленивой нагрузкой с
import
- Разделенный код по маршрутам/страницам, чтобы избежать загрузки ненужных вещей