本文主要记录本人对于 webpack 代码分离 和 缓存的学习。
一、代码分离
代码分离能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
常用的代码分离方法有三种:
- 入口起点 :使用
entry
配置手动地分离代码。 - 防止重复 :使用
Entry dependencies
或者SplitChunksPlugin
去重和分离 chunk。 - 动态导入:通过模块的内联函数调用来分离代码。
1.1 入口起点
通过手动的方式配置多个入口,但是这种方式会存在如下隐患。
- 如果入口 chunk 之间包含一些重复的模块,那么这些重复模块都会被引入到各个 bundle 中。
- 这种方法不够灵活,并且不能动态地拆分应用程序逻辑中的核心代码。
例如:现有如下两个入口文件index.js
, another.js
都引用了lodash
模块,代码如下:
index.js
javascript
import _ from 'lodash';
console.log(_.join(['index', 'entry'], ' '));
another.js
javascript
import _ from 'lodash';
console.log(_.join(['another', 'entry'], ' '));
webpack.config.js
javascript
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
}
打包结果如下:

可以看出同一个模块lodash
被重复打包到another.bundle.js
和index.bundle.js
中。
1.2 防止重复
针对上述问题,这里介绍如何移除重复的模块。
(1)入口依赖
在配置文件中配置 dependOn
选项,以在多个 chunk 之间共享模块:
javascript
entry: {
index: {
import: './src/index.js',
dependOn: 'shared'
},
another: {
import: './src/another.js',
dependOn: 'shared'
},
shared: 'lodash'
},

可以看到,loadsh
模块被单独打包到shared.bundle.js
中,这样就防止了重复引用。
如果想要在一个 HTML 页面上使用多个入口,还需设置:
javascript
module.exports {
...
optimization: {
runtimeChunk: 'single'
}
}
因为不同入口文件引用了同一模块,这个模块可能会被实例化多次,从而导致程序出现错误,具体可见Multiple Entry Points Per Page 。
可以看到,除了
shared.bundle.js
,index.bundle.js
和 another.bundle.js
之外,还生成了一个 runtime.bundle.js
文件。HMTL文件中引入该文件即可。
尽管 webpack 允许每个页面使用多入口,但在可能的情况下,应该避免使用多入口,而使用具有多个导入的单入口:entry: { page: ['./analytics', './app'] }
。这样可以获得更好的优化效果,并在使用异步脚本标签时保证执行顺序一致。
多入口:Webpack 会从每个入口点开始打包,生成独立的包。这意味着每个入口点都会生成一个独立的bundle。
单入口多个导入:多个模块打包到同一个bundle中。
例如:
entry: { page: ['./analytics', './app'] }
它表示的含义是数组最后一个文件./app
是资源的入口文件,数组其余文件会预先构建到入口文件。等价于./app
文件在先引入./analytics
(import './analytics';
),然后设置入口entry: { page: './app' }
(2)SplitChunksPlugin
如果存在大量公共模块的话,通过手动方式去配置 shared
, 不仅繁琐,而且会有遗漏。对此,SplitChunksPlugin
插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。
将配置修改如下:
javascript
module.exports = {
entry: {
index: './src/index.js',
another: './src/another.js'
},
...
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
再进行打包,可以看到公共模块lodash
被分离成单独的chunk了

1.3 动态导入
webpack 提供了两个类似的技术实现动态拆分代码。第一种,也是推荐选择的方式,是使用符合 ECMAScript 提案 的 import()
语法 实现动态导入。第二种则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure
。
例如,我们将 index.js
内容修改如下:
javascript
import('lodash').then(({default: _}) => {
console.log(_.join(['index', 'entry'], ' '));
})
webpack配置修改如下:
css
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
},
...
}
可以看到打包后的结果中,lodash
会被分离到一个单独的bundle中。

1.4 预获取/预加载模块
Webpack v4.6.0+ 增加了对预获取(prefetch)和预加载(preload)的支持。
在声明 import
时,使用下面这些内置指令,可以让 webpack 输出"resource hint",来告知浏览器:
- prefetch(预获取):将来某些导航下可能需要的资源
- preload(预加载):当前导航下可能需要资源
(1)预获取
试想一下下面的场景:现在有一个 HomePage
组件,该组件内部渲染了一个 LoginButton
组件,点击后按钮后可以按需加载 LoginModal
组件。
go
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');
上面的代码在构建时会生成
<link rel="prefetch" href="login-modal-chunk.js">
并追加到页面头部,指示浏览器在闲置时间预获取 login-modal-chunk.js
文件。
只要父 chunk 完成加载,webpack 就会添加预获取提示。
(2)预加载
预加载是一种预先加载文件或资源的技术,通常用于在需要时快速访问它们。
预加载的优点是,当用户需要访问已预加载的文件或资源时,它们可以立即被使用,从而减少加载时间。预加载的缺点是,它会增加页面的整体加载时间,因为需要预先加载所有文件或资源
预获取和预加载的区别
- 预加载 chunk 会在父 chunk 加载时以并行方式开始加载;而预获取 chunk 会在父 chunk 加载结束后开始加载。
- 预加载 chunk 具有中等优先级,并会立即下载;而预获取 chunk 则在浏览器闲置时下载。
- 预加载 chunk 会在父 chunk 中立即请求,用于当下时刻;而预获取 chunk 则用于未来的某个时刻。
- 浏览器支持程度不同。
预加载和懒加载的区别
- 预加载是加载所有资源,在页面加载时就已经把所有图片、数据等都加载完成,在页面打开后就可以直接使用,无需等待加载。
- 懒加载是当页面打开后,对页面上的图片等附件进行预先加载,当所有图片都加载完成,再显示该页面。懒加载可以减少不必要资源的加载,只在需要时才加载资源。
二、缓存
通常,webpack打包后的内容部署到服务器上后,浏览器便可访问服务器获取该资源,但这通常比较耗时。对此,浏览器通常会采用缓存技术。命中缓存可以降低网络流量,使网站加载速度更快。
然而,如果在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。对此,webpack提供了一些配置确保 webpack 编译生成的文件能够被客户端缓存;而在文件内容变化后,能够请求到新的文件。
2.1 配置输出文件文件名
通常情况下,我们会输出文件名设置为main.js
或bundle.js
这类静态字符串。但是这样每次打包后的资源文件名不变,这样可能会让浏览器认为其未更新,从而使用缓存版本。对此,webpack 提供了一种称为 substitution(可替换模板字符串) 的方式,通过带括号字符串来模板化文件名。
例如,webpack 提供了如下模替换模板字符串(通过 webpack 内部的TemplatedPathPlugin
):
可在 chunk 层面进行替换的内容:
模板 | 描述 |
---|---|
[id] | 此 chunk 的 ID |
[name] | 如果设置,则为此 chunk 的名称,否则使用 chunk 的 ID |
[chunkhash] | 此 chunk 的 hash 值,包含该 chunk 的所有元素 |
[contenthash] | 此 chunk 的 hash 值,只包括该内容类型的元素,受 optimization.realContentHash 影响) |
可在文件层面替换的内容:
模板 | 描述 |
---|---|
[file] | filename 和路径,不含 query 或 fragment |
[query] | 带前缀 ? 的 query |
[fragment] | 带前缀 # 的 fragment |
[base] | 只有 filename(包含扩展名),不含 path |
[path] | 只有 path,不含 filename |
[name] | 只有 filename,不含扩展名或 path |
[ext] | 带前缀 . 的扩展名(对 output.filename 不可用) |
假设我们现在将输入输出设置如下:
JavaScript
{
entry: {
index: './src/index.js'
},
output: {
filename: [name].[contenthash].bundle.js,
path: path.resolve(__dirname, 'dist')
}
}
如下图,这是打包后的资源文件名:

如果我们修改文件内容,再次执行 webpack 进行打包,可以看到打包后的资源文件名也发生了改变。

如果文件内容不变,再次执行 webpack 打包,官网说也会生成新的资源文件名。但 webpack5 好像会去直接使用缓存的文件,如下图所示:

2.2 提取引导模板
上述SplitChunksPlugin
可以用于将模块分离到单独的 bundle 中。webpack 还提供了一个优化功能,可以使用 optimization.runtimeChunk
选项将 runtime 代码拆分为一个单独的 chunk。将其设置为 single
以为所有 chunk 创建一个 runtime bundle。
由于像 lodash
或 react
这样的第三方库很少像本地源代码一样频繁修改,因此通常推荐将第三方库提取到单独的 vendor
chunk 中。这一步将减少客户端对服务器的请求,同时保证自身代码与服务器一致。
可以添加如下配置:
JavaScript
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
}
}
然后执行webpack打包结果如下:

可以看到, index.[].bundle.js 不再包含 loadsh
等资源,体积明显变小。
本文到这里就结束啦,主要内容源于文档。