- Оригинальный адрес: use long term caching
- Оригинальный автор: Ivan Akulov
- Аднсера перевода: Эффективно используйте постоянный кеш
- Переводчик: Чжоу Вэнькан
- Корректор: Ян Мэн,грязь кун
существуетОптимизация объема приложенийПосле этого следующей стратегией по сокращению времени загрузки приложения является кэширование. Кэшируйте ресурс в клиенте, чтобы потом каждый раз не загружать его повторно.
Управление версиями пакетов и использование заголовков кеша
Общий способ использования кеша:
-
Скажите браузеру кэшировать файл на долгое время (скажем, на год)
# Server header Cache-Control: max-age=31536000
⭐️ ПРИМЕЧАНИЕ: Если вы не знакомы с
Cache-Control
, см. статью Джейка Арчибальда:Лучшие практики о кэшировании. -
Когда файл изменяется, файл переименовывается, что заставляет браузер загрузить его снова:
<!-- 修改前 --> <script src="./index-v15.js"></script> <!-- 修改后 --> <script src="./index-v16.js"></script>
Этот метод говорит браузеру загрузить файл JS, кэшировать его, а затем использовать его кэшированную копию. Браузер будет запрашивать сеть только тогда, когда имя файла изменилось (или кэш стал недействительным через год).
С webpack вы можете сделать то же самое, но вместо номера версии вы указываете хэш файла. использовать[chunkhash]
Хэш-значение может быть записано в имя файла:
// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.<strong>[chunkhash]</strong>.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"
}
Расширенное чтение
- Jake Archibald Лучшие практики о кэшировании
Извлечение зависимостей и среды выполнения в отдельные файлы
полагаться
Зависимости приложения обычно менее часты, чем изменения кода в реальном приложении. Если вы переместите их в отдельные файлы, браузеры смогут кэшировать их независимо друг от друга, поэтому их не нужно повторно загружать каждый раз, когда в вашем приложении изменяется код.
Ключевой термин: В терминологии webpack отдельный файл с кодом приложения называетсяchunk. Мы будем использовать это имя в следующих статьях.
Для извлечения зависимостей в отдельные фрагменты требуются следующие три шага:
-
Замените имя выходного файла на
[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]
заменены.До сих пор, если вы построете свое приложение, этот кусок содержит весь код приложения - так же, как мы не выполняли шаги выше. Но изменения скоро последуют.
-
в вебпаке 4,могу
optimization.splitChunks.chunks: 'all'
Параметры для добавления в конфигурацию веб-пакета:// webpack.config.js (for webpack 4) module.exports = { optimization: { splitChunks: { chunks: 'all', } }, };
Эта опция включает интеллектуальное разделение кода. Используя эту функцию, webpack извлечет код сторонней библиотеки размером более 30 КБ (до сжатия и gzip). Он также может извлекать общий код — полезно, если ваша сборка приводит к нескольким пакетам. (Например:Если вы разделите приложение по маршрутизации).
В вебпаке 3Добавить кCommonsChunkPluginПлагин:
// webpack.config.js (for webpack 3) module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ // chunk 的名称将会包含依赖 // 这个名称会在第一步时被 [name] 所替代 name: 'vendor', // 这个函数决定哪个模块会被打入 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
Только в вебпаке 4). В 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
Браузер кэширует эти файлы по отдельности и повторно загружает их только в случае изменения кода.
Код времени выполнения веб-пакета
К сожалению, просто извлечь код из сторонней библиотеки недостаточно. Если вы хотите попробовать изменить что-то в коде приложения:
// index.js
…
…
// 例如,增加这句:
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 упакован, в дополнение к коду модуля, Bundle WebPack также содержитruntime- Небольшой сегмент может управлять кодом, выполняемым модулем. Когда вы разделяете код на несколько файлов, эта небольшая часть кода создает сопоставление между идентификатором фрагмента и соответствующим файлом:
// 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'),
}),
// 这个插件必须在 vendor 生成之后执行(因为 webpack 把运行时打进了最新的 chunk)
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',
// minChunks: Infinity 表示任何应用模块都不能打进这个 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
- «Лучшие практики для 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>
Среда выполнения — это не так много кода, и встраивание его в HTML может помочь нам сохранить HTTP-запросы (особенно важно в HTTP/1; менее важно в HTTP/2, но все же полезно).
Давайте посмотрим, как это сделать.
Если вы используете HtmlWebpackPlugin для генерации HTML
если вы используетеHtmlWebpackPluginдля создания файлов HTML, вам необходимоInlineSourcePlugin:
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
// Inline all files which names start with “runtime~” and end with “.js”.
// That’s the default naming of runtime chunks
inlineSource: 'runtime~.+\\.js',
}),
// This plugin enables the “inlineSource” option
new InlineSourcePlugin(),
],
};
Если вы создаете HTML с пользовательской логикой на стороне сервера
В веб-пакете 4:
-
Добавить кWebpackManifestPluginПлагин может получить имя сгенерированного фрагмента runtume:
// 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
, вы можете оставить имя среды выполнения без изменений:// webpack.config.js (for webpack 3) module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, filename: 'runtime.js', // → Now the runtime file will be called // “runtime.js”, not “runtime.79f17c27b335abc7aaf4.js” }), ], };
-
можно вставить удобным способом
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()
функция загрузки.
Это позволяет入口
Бандл становится меньше, тем самым сокращается время первой загрузки. Мало того, он еще и кеш может оптимизировать - если изменить код входа CHUNK, то комментарий CHUNK не пострадает.
⭐️ Примечание. Если вы используете Babel для компиляции кода, он не будет распознан Babel.
import()
Произошла синтаксическая ошибка. Чтобы избежать этой ошибки, вы можете добавитьsyntax-dynamic-import
плагин.
Расширенное чтение
- Документация по веб-пакету
import()
использование функций - Предложение JavaScriptвыполнить
import()
грамматика
Разделить код на маршруты и страницы
Если ваше приложение имеет несколько маршрутов или страниц, но только один JS-файл в коде (один入口
chunk), что, кажется, добавляет дополнительный трафик к каждому из ваших запросов. Например, когда пользователь посещает домашнюю страницу вашего веб-сайта:
Им не нужно загружать код на другие страницы, которые отображают статью, но они это делают. Кроме того, если этот пользователь часто посещает только домашнюю страницу, но вы измените код статьи на других страницах, веб-пакет перекомпилирует весь пакет, что заставит пользователя повторно загрузить весь код приложения.
Если мы разделим код на страницы (или маршруты в одностраничном приложении), пользователи будут загружать только те части кода, которые они действительно используют. Кроме того, браузеры лучше кэшируют код приложения: когда вы изменяете код на главной странице, веб-пакет делает недействительным только соответствующий фрагмент.
одностраничное приложение
Чтобы разделить одностраничное приложение путем маршрутизации, вы можете использоватьimport()
(см. вышеленивая загрузка кодачасть). Если вы используете фреймворк, есть и готовые решения:
-
react-router
в документации«Разделение кода»(для реагирования) -
vue-router
в документации"Ленивая загрузка маршрутов"(для Vue.js)
Традиционные многостраничные приложения
Чтобы разделить традиционное приложение по страницам, вы можете использовать веб-пакетentry points. Предположим, что в вашем приложении есть три типа страниц: домашняя страница, страница статьи и страница учетной записи пользователя, — тогда должно быть три записи:
// webpack.config.js
module.exports = {
entry: {
home: './src/Home/index.js',
article: './src/Article/index.js',
profile: './src/Profile/index.js'
},
};
Для каждого файла записи webpack построит отдельное дерево зависимостей и сгенерирует пакет, содержащий только модули, используемые записью:
$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./home.91b9ed27366fe7e33d6a.js 18 kB 1 [emitted] home
./article.87a128755b16ac3294fd.js 32 kB 2 [emitted] article
./profile.de945dc02685f6166781.js 24 kB 3 [emitted] profile
./vendor.4f14b6326a80f4752a98.js 46 kB 4 [emitted] vendor
./runtime.318d7b8490a7382bf23b.js 1.45 kB 5 [emitted] runtime
Таким образом, если только страница статьи использует Lodash, комплекты домашней страницы и профиля не будут включать его, и пользователь не загрузит библиотеку при посещении домашней страницы.
Однако отдельные деревья зависимостей имеют свои недостатки. Если оба портала используют Lodash и вы не переместите зависимости в пакет поставщика, оба портала будут содержать копию Lodash. Для решения этой проблемы,в вебпаке 4, который можно добавить в конфигурацию вашего веб-пакетаoptimization.splitChunks.chunks: 'all'
Опции:
// webpack.config.js (适用于webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
}
},
};
Эта опция включает интеллектуальное разделение кода. С этой опцией webpack автоматически найдет общий код и извлечет его в отдельный файл.
в вебпаке 3,можно использоватьCommonsChunkPlugin
плагин, который переместит общие зависимости в новый именованный файл:
// webpack.config.js (适用于 webpack 3)
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
// chunk 的名称将会包含公共依赖
name: 'common',
// minChunks表示要将一个模块打入公共文件时必须包含的 `minChunks` chunks 数量
// (注意,插件会分析所有 chunks 和 entries)
minChunks: 2, // 2 is the default value
}),
],
};
можно попробовать настроитьminChunks
значения для поиска оптимального решения. Как правило, вы хотите, чтобы это было небольшое значение, но оно будет увеличиваться по мере увеличения количества фрагментов. Например, когда есть 3 чанка,minChunks
может быть 2, но с 30 фрагментами это может быть 8, потому что, если вы установите его на 2, у вас будет много модулей, которые нужно упаковать в один и тот же общий файл, поэтому файл станет раздутым.
Расширенное чтение
- Документация по веб-пакетуКонцепция точек входа
- Документация по веб-пакетуО плагине CommonsChunkPlugin
- «Лучшие практики для CommonsChunkPlugin»
optimization.splitChunks
а такжеoptimization.runtimeChunk
как это работает
Убедитесь, что идентификатор модуля более стабилен
При создании кода webpack присваивает каждому модулю идентификатор. Впоследствии эти идентификаторы будутrequire()
используется в функции. Обычно вы видите эти идентификаторы перед путем к модулю в скомпилированном выводе:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.4e50a16675574df6a9e9.js 60 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
↓ ↓ см. ниже
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module
По умолчанию эти идентификаторы рассчитываются с помощью счетчиков (например, идентификатор первого модуля равен 0, идентификатор второго модуля равен 1 и т. д.). Но с этим есть проблема, когда вы добавляете модуль, он может оказаться в середине списка модулей, из-за чего ID всех модулей потом будет изменен:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.5c82c0f337fcb22672b5.js 22 kB 0 [emitted]
./main.0c8b617dfc40c2827ae3.js 82 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
↓ Мы добавили новый модуль...
[4] ./webPlayer.js 24 kB {1} [built]
↓ ↓ Смотрите, что внизу!comments.js
ID изменен с 4 на 5
[5] ./comments.js 58 kB {0} [built]
↓ads.js
ID изменен с 5 на 6
[6] ./ads.js 74 kB {1} [built]
+ 1 hidden module
Это сделает недействительными все фрагменты, которые содержат или зависят от модулей с этими измененными идентификаторами, даже если их фактический код не изменился. В нашем случае идентификатор0
кусок (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(),
],
};
Расширенное чтение
- Документация по веб-пакетуО HashedmoduleiDsplugin.
Суммировать
- Кэширование пакетов и управление версиями путем изменения имен пакетов
- Разделить пакет на код приложения (приложения), код поставщика (сторонней библиотеки) и среду выполнения.
- Встроенная среда выполнения сохраняет HTTP-запросы
- использовать
import
Ленивая загрузка некритического кода - Разделите код по маршруту или странице, чтобы избежать загрузки ненужных файлов