之前写的《webpack入门必备(一):基础配置》主要介绍了webpack基础解析所需的loader/plugin。而随着日常webpack的使用,我们会更多关注如何构建更快、构建产物更小、构建产物符合规范...希望这篇文章可以让你找到答案。
一、webpack4的构建优化
1. 加快构建速度
1.1 优化配置
这里介绍的主要的几种优化配置如下所示:
缩小构建范围
- exclude、include范围
- noParse
- IgnorePlugin
多进程
- thread-loader/happypack
缓存
- cache-loader/cacheDirectory,把loader的处理结果缓存到本地
- Dll缓存,把一些不常变更的模块构建产物缓存在本地
如果你有没用过的配置可以接着看下面的具体使用方法,如果你已经很熟悉了则可以跳过此节~
1. exclude、include范围
配置来确保转译尽可能少的文件(exclude 的优先级高于 include)
const rootDir = process.cwd();
{
test: /\.(j|t)sx?$/,
include: [path.resolve(rootDir, 'src')],
exclude: [
/(.|_)min\.js$/
],
}
PS. 相比exclude可以多用include
2. noParse
如果一些库不依赖其它库的库,不需要解析他们,可以引入来加快编译速度。
noParse: /node_modules\/(moment|chart\.js)/
3. IgnorePlugin
忽略第三方包指定目录。 (他是webpack 内置的插件)
例如: moment (2.24.0版本) 会将所有本地化内容和核心功能一起打包,我们就可以使用 IgnorePlugin 在打包时忽略本地化内容(语言包),见下图。
plugins: [
// 表示忽略moment下的locale文件夹内容
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]
4.1 thread-loader
把 thread-loader 放置在其它 loader 之前,那么它之后的 loader 就会在一个单独的 worker 池中运行。
// 项目中babel-loader一般耗时比较长,所以可以配置thread-loader
rules: [
{
test: /\.jsx?$/,
use: ['thread-loader', 'cache-loader', 'babel-loader']
}
]
4.2 happypack
运行在Node.js上的webpack是单线程,将文件解析的任务拆分由多个子进程并发进行,然后子进程处理完任务后再将结果发送给主进程,提升项目构件速度。
(但是因为进程的分配和管理也需要时间,所以使用后不一定快,需要项目接入实验一下)
const Happypack = require("happypack");
module.exports = {
module: {
rules: [
{
test: /\.js[x]?$/,
use: "Happypack/loader?id=js",
include: [path.resolve(__dirname, "src")],
},
{
test: /\.css$/,
use: "Happypack/loader?id=css",
include: [
path.resolve(__dirname, "src"),
path.resolve(__dirname, "node_modules", "bootstrap", "dist"),
],
},
{
test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2|.gexf)$/,
use: "Happypack/loader?id=file",
include: [
path.resolve(__dirname, "src"),
path.resolve(__dirname, "public"),
path.resolve(__dirname, "node_modules", "bootstrap", "dist"),
],
},
],
},
plugins: [
new Happypack({
id: "js", //和rule中的id=js对应
//将之前 rule 中的 loader 在此配置
use: ["babel-loader"], //必须是数组
}),
new Happypack({
id: "css", //和rule中的id=css对应
use: ["style-loader", "css-loader", "postcss-loader"],
}),
new Happypack({
id: "file", //和rule中的id=file对应
use: [
{
loader: "url-loader",
options: {
limit: 10240, //10K
},
},
],
}),
],
};
5. cache-loader/cacheDirectory
在性能开销较大的loader处使用,将构建结果缓存中磁盘中。
(默认存在node_modueles/.cache/cache-loader目录下。 )
cacheDirectory例子:
rules: [
{
test: /\.(j|t)sx?$/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
}
}
]
cache-loader例子:
rules: [
{
test: /\.(css)$/,
use: [
{ loader: 'style-loader' },
{ loader: 'cache-loader' },
{ loader: 'css-loader' },
{ loader: 'postcss-loader' }
]
}
]
6. Dll缓存(动态链接库)
将复用性较高的第三方模块打包到DLL中,再次构建时直接复用,这样只需重新打包业务代码。
(注意是DLL缓存是大大缩短了首次构建时间
,像之前的cache-loader优化都是缩短rebuild时间
)
使用相关插件:
- DllPlugin 插件:用于打包出一个个单独的动态链接库文件。
- DllReferencePlugin 插件:用于在主要配置文件中去引入 DllPlugin 插件打包好的动态链接库文件。
具体步骤:
(1) 新增一个webpack配置去编译DLL文件([name].dll.js
、[name].manifest.json
)
// 新增一个webpack-dll.config.js配置文件
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
const distPath = path.resolve(__dirname, 'dll');
module.exports = {
entry: {
// 把 React 相关模块的放到一个单独的动态链接库
react: ['react', 'react-dom'],
// 把项目需要所有的 polyfill 放到一个单独的动态链接库
polyfill: [
'core-js/fn/object/assign',
'core-js/fn/object/entries',
...
],
},
output: {
// 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称(react 和 polyfill)
filename: '[name].dll.js',
path: distPath,
// 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
// 之所以在前面加上 _dll_ 是为了防止全局变量冲突
library: '_dll_[name]',
},
plugins: [
// 接入 DllPlugin
new DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值(_dll_react)
name: '_dll_[name]',
context: process.cwd(),
// 描述动态链接库的 manifest.json 文件输出时的文件名称
path: path.join(__dirname, 'dll', '[name].manifest.json'),
}),
],
};
// package.json里新增dll的构建命令
"scripts": {
"dll": "webpack --config webpack-dll.config.js",
}
(2) dev构建时,告诉 Webpack 使用了哪些动态链接库
// webpack.config.js文件
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
plugins: [
// 使用的动态链接库(react和polyfill的)
new DllReferencePlugin({
context: process.cwd(),
manifest: path.join(rootDir, 'dll', 'react.manifest.json'),
}),
new DllReferencePlugin({
context: process.cwd(),
manifest: path.join(rootDir, 'dll', 'polyfill.manifest.json'),
}),
...
]
(3) html template里引入文件
因为我这里只是本地构建加速,所以就以dev的方式引入
<script src="./dll/polyfill.dll.js?_dev"></script>
<script src="./dll/react.dll.js?_dev"></script>
到这DLL就配好了。有些人可能比较好奇react.dll.js
和react.manifast.js
到底是什么文件,做了什么事?你看看他两个文件就知道啦~
react.dll.js
其实主要就是所引用模块的代码集合react.manifast.js
则写明包含哪些模块、模块路径
// react.dll.js文件部分内容如下所示。
var _dll_react = (function(modules) {
// ... 此处省略 webpackBootstrap 函数代码
}([
function(module, exports, __webpack_require__) {
// 模块 ID 为 0 的模块对应的代码
},
function(module, exports, __webpack_require__) {
// 模块 ID 为 1 的模块对应的代码
},
// ... 此处省略剩下的模块对应的代码
]));
// react.manifast.js文件部分内容如下所示。
{
// 描述该动态链接库文件暴露在全局的变量名称
"name": "_dll_react",
"content": {
"./node_modules/process/browser.js": {
"id": 0,
"meta": {}
},
// ... 此处省略部分模块
"./node_modules/react-dom/lib/ReactBrowserEventEmitter.js": {
"id": 42,
"meta": {}
},
...
}
1.2 检测工具
常用工具:speed-measure-webpack-plugin
使用方法:用其来包裹 Webpack 的配置
2. 构建产物方面
2.1 减小构建产物大小、提高复用率
这里介绍的主要的几种优化配置如下所示:
- optimization.splitChunks分包
- babel配置
@babel/plugin-transform-runtime
- tree-shaking
具体使用:
1. optimization.splitChunks分包
将业务代码和第三方依赖库进行分包,减小index.js的大小;
抽离多页应用的公共模块,单独打包。公共代码只需要下载一次就缓存起来了,避免了重复下载。
optimization: {
minimize: false,
moduleIds: 'named',
splitChunks: {
chunks: 'all',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 6,
maxInitialRequests: 6,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
polyfill: {
test: /[\\/]node_modules[\\/](core-js|@babel|regenerator-runtime)/,
name: 'polyfill',
priority: 70,
minChunks: 1,
reuseExistingChunk: true
},
lib: {
test: /[\\/]node_modules[\\/]/,
name: 'lib',
chunks: 'initial',
priority: 3,
minChunks: 1,
},
...
}
}
}
2. babel配置
提取所有页面所需的helper函数到一个包里,避免重复注入
"plugins": [
"@babel/plugin-transform-runtime"
...
]
3. tree-shaking
如果使用ES6的import 语法,那么在生产环境下,会自动移除没有使用到的代码。
(1) 具体配置
const TerserPlugin = require('terser-webpack-plugin');
const config = {
// 生产模式下tree-shaking才生效
mode: 'production',
optimization: {
// Webpack 将识别出它认为没有被使用的代码,并在最初的打包步骤中给它做标记。
usedExports: true,
minimizer: [
// 删除死代码的压缩器
new TerserPlugin({...})
]
}
};
(2) 哪类代码会被shake掉?以下有一些事例
// no tree-shaking
import Stuff from './stuff';
doSomething(Stuff);
// tree-shaking
import Stuff from './stuff';
doSomething();
// tree-shaking
import './stuff';
doSomething();
// no tree-shaking
import 'my-lib';
doSomething();
// 全部导入 no tree-shaking
import _ from 'lodash';
// 具名导入 tree-shaking
import { debounce } from 'lodash';
// 直接导入具体的模块 tree-shaking
import debounce from 'lodash/lib/debounce';
(3) 什么叫有副作用的代码?
`只要被引入,就会对应用程序产生重要的影响。
(一个很好的例子就是全局样式表,或者设置全局配置的js文件。)`
(4) 有副作用的代码我们不希望被shake,我们可以配置如下
// 所有文件都有副作用,全都不可 tree-shaking
{
"sideEffects": true
}
// 没有文件有副作用,全都可以 tree-shaking
{
"sideEffects": false
}
// 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
{
"sideEffects": [
"./src/file1.js",
"./src/file2.js"
]
}
(5) 注意,babel配置需要配modules: false
,忽略import/export代码编译
const config = {
presets: [
[
'@babel/preset-env',
{
// commonjs代码不能被tree-shaking
// 所以babel保留我们现有的 es2015 import/export 语句,不进行编译
modules: false
}
]
]
};
2.2 检测工具
常用工具:webpack-bundle-analyzer
使用方法:用其来包裹 Webpack 的配置
3. 产物检查
ES check
生产环境构建时,会检查构建产物里是否存在es6语法。有则抛出错误并提示你去进行babel编译,这样避免了构建产物不合要求的情况。
具体使用例子:
// package.json 命令里加上es-check检查
"dist:basic": "rimraf public && cross-env NODE_ENV=production webpack --config webpack-dist.config.js && es-check es5 ./public/**/*.js"
二、webpack5的构建优化
1. 速度优化
1.1 编译缓存
编译缓存就是在首次编译后把结果缓存起来,在后续编译时复用缓存,从而达到加速编译的效果。
webpack5默认开启编译缓存,缓存默认是在内存里,你可以自定义。
module.exports = {
cache: {
// 将缓存类型设置为文件系统
type: "filesystem",
// 缓存的位置(默认是node_modules/.cache/webpack)
cacheDirectory: path.resolve(__dirname, '.temp_cache'),
// 指定构建过程中的代码依赖。webpack将使用这些项目以及所有依赖项的哈希值来使文件系统缓存无效。
buildDependencies: {
// 当配置文件内容或配置文件依赖的模块文件发生变化时,当前的构建缓存即失效。
config: [__filename],
// webpack.config、loader和所有从你的配置中require的模块都会被自动添加。如果有其他的东西被构建依赖,你可以在这里添加它们
},
// 指定缓存的版本。当需要更新配置缓存时,通过设置此版本使缓存失效。
version: '1.0'
}
}
一些参数注解
cache: true
就是cache: { type: 'memory' }
的别名type
: 'filesystem'|'memory'。
如果设置'memory'则缓存在内存且不能配置其他信息,设置成'filesystem'就可以配置更多信息。默认开发模式使用的是'memory',生产模式是false。
version
: 当配置文件和代码都没有发生变化,但是构建的外部依赖(如环境变量)发生变化时,预期的构建产物代码也可能不同。这时就可以使用 version 配置来防止在外部依赖不同的情况下混用了相同的缓存。例如,可以传入 cache: {version: process.env.NODE_ENV},达到当不同环境切换时彼此不共用缓存的效果。
1.2 长效缓存 Long-term caching
长效缓存指的是能充分利用浏览器缓存,尽量减少由于模块变更导致的构建文件hash值的改变,从而导致文件缓存失效。
(由于moduleId和chunkId确定了,构建的文件的hash值也会确定。)
1.2.1 引子
chunk、module都是什么?
- module:每一个可被导入导出的源码js、css文件就是一个module。
- chunk:module经webpack依赖分析、打包生成的单独文件块。如:入口entry里的文件、SplitChunks抽离的公共代码
- bundle:chunk后面经过编译/压缩打包等处理后就变成了bundle,bundle文件直接被html文件引用。
- webpack提供了以下3种哈希值,分别是什么意思?有啥优缺点?
- hash 所有bundle文件都是同一个hash。(【缺点】不修改文件的情况下rebuild后hash会更新)
- chunkhash 同一个entry/及entry引用的chunk文件都是同一个hash。(【缺点】修改chunk文件内容后,这个hash不变)
- contenthash 一个文件一个hash,修改哪个文件哪个文件的hash就改变。(【缺点】如果删除一个entry里的chunk,entry和chunk及好多个文件的hash都变了,不利于长效缓存。
比如只是jsx删除引用的一个css文件 好多bundle文件的hash就都变了。)
1.2.2 webpack4实现长效缓存
之前需要通过如下配置达到长效缓存:
plugins: [
- new webpack.NamedModulesPlugin(),
+ new webpack.HashedModuleIdsPlugin(),
或者配置
optimization.moduleIds = 'hashed’
optimization.chunkIds = 'named'
配置说明:
- 在开发环境下使用 NamedModulesPlugin 来固化 module id,在生产环境下使用 HashedModuleIdsPlugin 来固化 module id(因为构建结果文件会更小)
- 使用 NamedChunksPlugin 来固化 runtime 内以及在使用动态加载时分离出的 chunk 的 chunk id
(NamedChunksPlugin 只能对普通的 Webpack 模块起作用,异步模块(异步模块可以在 import 的时候加上 chunkName 的注释,比如这样:import(/ webpackChunkName: “lodash” / ‘lodash’).then() 这样就有 Name 了),external 模块是不会起作用的。)
1.2.3 Webpack5默认启用deterministic实现长效缓存
Webpack5采用新的算法,生产模式下默认启用如下配置不仅实现长效缓存,还减少了文件打包大小:
optimization.chunkIds: "deterministic"
optimization.moduleIds: "deterministic"
mangleExports: “deterministic"
PS.具体采用的算法还需要进一步深入研究~
2. 包构建大小优化
2.1 Node Polyfill脚本被移除
Webpack 4版本附带了大多数Node.js核心模块的polyfill,一旦前端使用了任何核心模块,这些模块就会自动应用,导致polyfill文件很大,但是其实有些polyfill是不必要的。
而现在webpack5将不会自动为Node.js模块添加Polyfills,需要开发者手动添加合适的Polyfills。
升级迁移至webpack5需要注意:
- 尽可能尝试使用与前端兼容的模块。
- 可以为 Node.js 核心模块手动添加 polyfill。错误消息将提示实现方法。
- 包作者:使用 package.json 中的 browser 字段使包与前端兼容。提供浏览器的替代实现 / 依赖。
2.2 tree-shaking
1.嵌套tree-shaking
能够跟踪对export的嵌套属性的访问,分析模块的export和import的依赖关系,去掉未被使用的模块
// inner.js
export const a = 1;
export const b = 2;
// module.js
export * as inner from './inner';
// or import * as inner from './inner'; export { inner };
// user.js
import * as module from './module';
console.log(module.inner.a); // 在此示例中,可以在生产模式下移除导出 b。
2.内部模块tree-shaking(深度作用域分析)
新属性optimization.innerGraph分析模块导出和导入之间的依赖关系,在生产模式下默认启用。
import { something } from './something';
function usingSomething() {
return something;
}
export function test() {
return usingSomething();
}
// 在使用 test 导出时才使用 something。
可以分析以下符号:
- 函数声明
- 类声明
- 带有以下内容的 export default 或变量声明:函数表达式,类表达式,序列表达式,/#_PURE_/ 表达式,局部变量,导入绑定
3.package.json 中的“sideEffects”标志允许将模块手动标记为无副作用,从而在不使用它们时将其移除。
webpack 5 还可以根据对源代码的静态分析,自动将模块标记为无副作用。
更多Webpack5的内容推荐阅读:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。