- 原文地址: use long term caching
- 原文做者: Ivan Akulov
- 译文地址: 利用好持久化缓存
- 译者: 周文康
- 校对者: 闫蒙、泥坤
在优化应用体积以后,下一个提高应用加载时间的策略就是缓存。将资源缓存在客户端中,能够避免以后每次都从新下载。html
使用缓存的通用方法:前端
告诉浏览器须要缓存一个文件很长时间(好比,一年)vue
# Server header
Cache-Control: max-age=31536000
复制代码
⭐️ 注意:若是你不熟悉
Cache-Control
的原理,请参阅 Jake Archibald 的文章: 关于缓存的最佳实践。node
当文件改变时,文件会被重命名,这样就迫使浏览器从新下载:react
<!-- 修改前 -->
<script src="./index-v15.js"></script>
<!-- 修改后 -->
<script src="./index-v16.js"></script>
复制代码
这个方法能够告诉浏览器去下载 JS 文件,并将它缓存,以后使用的都是它的缓存副本。浏览器只会在文件名发生改变(或者一年以后缓存失效)时才会请求网络。webpack
使用 webpack,一样能够作到,但使用的不是版本号,而是指定文件的哈希值。使用 [chunkhash]
能够将哈希值写入文件名中:git
// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.<strong>[chunkhash]</strong>.js',
// → bundle.8e0d62a03.js
},
};
复制代码
⭐️ 注意: 即便 bundle 不变,webpack 也可能生成不一样的哈希值 – 例如,你重命名了一个文件或者在不一样的操做系统下编译了 bundle。 固然,这实际上是一个 bug,目前尚未明确的解决方案,具体可参阅 GitHub 上的讨论。github
若是你须要将文件名发送给客户端,可使用 HtmlWebpackPlugin
或者 WebpackManifestPlugin
。web
HtmlWebpackPlugin
是一个简单但扩展性不强的插件。在编译期间,它会生成一个 HTML 文件,文件包含了全部已经被编译的资源。若是你的服务端逻辑不是很复杂,那么它应该能知足你:vue-router
<!-- index.html -->
<!doctype html> <!-- ... --> <script src="bundle.8e0d62a03.js"></script> 复制代码
WebpackManifestPlugin
是一个扩展性更佳的插件,它能够帮助你解决服务端逻辑比较复杂的那部分。在打包时,它会生成一个 JSON 文件,里面包含了原文件名和带哈希文件名的映射。在服务端,经过这个 JSON 就能方便的找到咱们真正要执行的文件:
// manifest.json
{
"bundle.js": "bundle.8e0d62a03.js"
}
复制代码
应用的依赖一般比实际应用内的代码变动频率低。若是将它们移到单独的文件中,浏览器就能够独立缓存它们 – 这样每次应用中的代码变动也不会去从新下载它们。
关键术语:在 webpack 术语中,把带有应用代码的独立文件称之为 chunk。咱们在下面的文章中会使用到这个名称。
要将依赖项提取到独立的 chunk 中,须要执行下面三个步骤:
将输出文件名替换为[name].[chunkname].js
:
// webpack.config.js
module.exports = {
output: {
// Before
filename: 'bundle.[chunkhash].js',
// After
filename: '[name].[chunkhash].js',
},
};
复制代码
当 webpack 编译应用时,它会将[name]
做为 chunk 的名称。若是咱们没有添加 [name]
的部分,咱们将不得不经过哈希值来区分 chunk - 这样就变得很是困难!
将 entry
的值改成对象:
// webpack.config.js
module.exports = {
// Before
entry: './index.js',
// After
entry: {
main: './index.js',
},
};
复制代码
在上面这段代码中,“main” 是 chunk 的名称。这个名称会在第一步时被 [name]
所替代。
到目前为止,若是你构建应用,这个 chunk 仍是包含了整个应用的代码 - 就像咱们没有作过上述这些步骤同样。但接下来很快就将产生变化。
在 webpack 4 中,能够将 optimization.splitChunks.chunks: 'all'
选项添加到 webpack 的配置中:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
}
},
};
复制代码
这个选项能够开启智能代码拆分。使用了这个功能,webpack 将会提取大于 30KB(压缩和 gzip 以前)的第三方库代码。它同时也能够提取公共代码 - 若是你的构建结果会生成多个 bundle 时这将很是有用。(例如:假如你经过路由来拆分应用)。
在 webpack 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
只有在 webpack 4 才有)。在 webpack 4 中,若是依赖项很小,则可能不会生成 vendor bundle - 这点作的不错:
$ 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 打包时,除了模块代码以外,webpack 的 bundle 中还包含了 runtime - 一小段能够管理模块执行的代码。当你将代码拆分红多个文件时,这小部分代码在 chunk id 和匹配的文件之间会生成一个映射:
// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
"0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";
复制代码
Webpack 将 runtime 包含在了最新生成的 chunk 中,这个 chunk 就是咱们代码中的 vendor
。每次 chunk 有任何变动,这一小部分代码也会随之更改,同时也会致使整个 vendor
chunk 发生改变。
为了解决这个问题,咱们能够将 runtime 移动到一个独立的文件中。在 webpack 4 中,能够经过开启 optimization.runtimeChunk
选项来实现:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
runtimeChunk: true,
},
};
复制代码
在 webpack 3 中,能够经过 CommonsChunkPlugin
建立一个额外的空 chunk:
// 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>
复制代码
optimization.splitChunks
和 optimization.runtimeChunk
的工做原理为了达到更好的体验,咱们能够尝试把 webpack 的 runtime 内联到 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>
复制代码
Runtime 的代码很少,内联到 HTML 中能够帮助咱们节省 HTTP 请求(在 HTTP/1 中尤其重要;在 HTTP/2 中虽然没那么重要,但仍然能起到必定做用)。
下面就来看看要如何作。
若是你使用 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(),
],
};
复制代码
在 webpack 4 中:
添加 WebpackManifestPlugin 插件能够获取生成的 runtume chunk 的名称:
// 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"
}
复制代码
能够用一个便利的方式内联 runtime chunk 的内容。例如,使用 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
,可使 runtime 的名称不发生改变 :
// 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> … `);
});
复制代码
一般,一个网页会有自身的侧重点:
上面的这些状况,均可以经过优先下载最重要的部分,稍后懒加载剩余部分,从而来提高页面首次加载的性能。在 webpack 中,使用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')
,就会把这个模块放到单独的 chunk 中:
$ 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()
函数时才会去下载。
这样可让 入口
bundle 变得更小,从而减小首次加载时间。不只如此,它还能够优化缓存 - 若是你修改了入口 chunk 的代码,注释 chunk 不会受到影响。
⭐️ 注意: 若是你使用 Babel 编译代码,会由于 Babel 没法识别
import()
而出现语法错误。为了不这个错误,你能够添加syntax-dynamic-import
插件。
import()
函数的使用import()
语法若是你的应用有多个路由或页面,可是代码中只有一个单独的 JS 文件(一个单独的入口
chunk),这样彷佛会让你的每次请求都附加了额外的流量。例如,当用户访问你网站的首页:
他们并不须要加载其它页面上用于渲染文章的代码 - 但他们却加载了。此外,若是这个用户常常只是访问首页,但你更改了其它页面的文章代码,webpack 将会从新编译,使整个 bundle 失效 - 这样将致使用户从新下载整个应用的代码。
若是咱们将代码拆分到页面中(或者单页面应用的路由里),用户就会只下载真正用到的那部分代码。此外,浏览器也会更好地缓存应用代码:当你改变首页的代码时,webpack 只会让相匹配的 chunk 失效。
要经过路由来拆分单页应用,可使用 import()
(参加上文代码懒加载部分)。若是你使用的是一个框架,目前也有现成的解决方案:
要经过页面来拆分传统应用,可使用 webpack 的 entry points。假设你的应用中有三类页面:主页、文章页和用户帐户页,- 那么就应该有三个入口:
// webpack.config.js
module.exports = {
entry: {
home: './src/Home/index.js',
article: './src/Article/index.js',
profile: './src/Profile/index.js'
},
};
复制代码
对于每一个入口文件,webpack 将构建一个单独的依赖树并生成一个 bundle,这个 bundle 里只有包含这个入口所使用到的模块:
$ 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
复制代码
因此,若是只有 article 页面使用到了 Lodash,那么 home 和 profile bundle 就不会包含它 - 用户也不会在访问首页的时候下载到这个库。
可是,单独的依赖树有它们的缺点。若是两个入口都使用到了 Lodash,同时你没有将依赖项移到 vendor bundle 中,则两个入口都将包含 Lodash 的副本。为了解决这个问题,在 webpack 4 中,能够在你的 webpack 配置中加入optimization.splitChunks.chunks: 'all'
选项:
// webpack.config.js (适用于webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
}
},
};
复制代码
这个选项能够开启智能代码拆分。有了这个选项,webpack 将自动查找到公共代码,而且提取到单独的文件中。
在 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
的值来找到最优的方案。一般状况下,你但愿它是一个较小的值,但随着 chunk 数量的增长它会随之增大。例如,有 3 个 chunk 时,minChunks
的值多是 2 ,可是有 30 个 chunk 时,它的值多是 8 - 由于若是你把它设置成 2,就会有不少模块要被打包进同一个公共文件中,这样文件就会变得臃肿。
optimization.splitChunks
和 optimization.runtimeChunk
的工做原理构建代码时,webpack 会为每一个模块分配一个 ID。随后,这些 ID 将在 bundle 里的 require()
函数中被使用到。你一般会在编译输出的模块路径前看到这些 ID:
$ 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 是使用计数器计算出来的(例如,第一个模块的 ID 是 0,第二个模块的 ID 就是 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
复制代码
这将使包含或依赖于这些被更改 ID 的模块的全部 chunk 都无效 - 即便它们实际代码没有更改。在咱们的案例中,ID 为 0
的 chunk ( comments.js
的 chunk) 和 main
chunk (其它应用代码的 chunk )都将失效 - 但其实只有 main
应该失效。
为了解决这个问题,可使用 HashedModuleIdsPlugin
插件来改变模块 ID 的计算方式。这个插件用模块路径的哈希值代替了基于计数器的 ID:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.6168aaac8461862eab7a.js 22.5 kB 0 [emitted]
./main.a2e49a279552980e3b91.js 60 kB 1 [emitted] main
./vendor.ff9f7ea865884e6a84c8.js 46 kB 2 [emitted] vendor
./runtime.25f5d0204e4f77fa57a1.js 1.45 kB 3 [emitted] runtime
复制代码
↓ 看下面
[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
+ 1 hidden module
复制代码
使用了这个方法,只有当重命名或移动该模块时,模块的 ID 才会更改。新的模块也不会影响到其余模块的 ID。
能够在配置中的 plugins
部分开启这个插件:
// webpack.config.js
module.exports = {
plugins: [
new webpack.HashedModuleIdsPlugin(),
],
};
复制代码
import
懒加载非关键代码