最近在系统整理知识点,现将 Webpack 的一些重要知识点罗列出来,方便快速查阅。
Tree Shaking
为了使用tree shaking
,需要满足以下条件:
- 使用 ES2015 语法 (即
import
和export
) - 在项目
package.json
文件中,添加sideEffects
入口 - 引入一个能够删除未引用代码 (dead code) 的压缩工具(minifier)(例如:UglifyJSPlugin)
将文件标记为无副作用(side-effect-free)
这种方式是通过 package.json
的sideEffects
属性来实现的。
{"sodeEffects": false}
「副作用」的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export
或多个export
。举例说明,例如polyfill
,它影响全局作用域,并且通常不提供export
。
注意,任何导入的文件都会受到 tree shaking
的影响。这意味着,如果在项目中使用类似 css-loader
并导入 CSS
文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:
{"sideEffects": ['*.css']
}
压缩输出
从 webpack 4 开始,也可以通过 “mode” 配置选项轻松切换到压缩输出,只需设置为 “production”。
也可以在命令行接口中使用 --optimize-minimize
标记,来使用UglifyJSPlugin
。
Code Splitting
code splitting
的必要性
- 不进行
code splitting
,打包后单文件提交较大,加载时长较长,影响用户体验 - 不进行
code splitting
,经常修改业务代码,重新打包后,浏览器不能进行缓存,导致性能较差,影响用户体验
code splitting
的配置
同步代码方式
import _ from 'lodash';
webpack.common.js
配置如下:
....
optimization: {
splitChunks: {chunks: 'all'}
}
....
配置后,会将公用类库进行打包,生成一个 vendors~main.js
文件。
异步代码方式
function getComponent() {return import('lodash').then(({default: _}) => {var element = document.createElement('div');
element.innerHTML = _.join(['Clear', 'love'], '');
return element;
})
}
getComponent().then(element => {document.body.appendChild(element);
})
SplitChunksPlugin 配置参数详解
魔法注释 (magic comment
) 修改打包动态组件名称
1. 使用 @babel/plugin-syntax-dynamic-import
支持动态引入插件
在 .babelrc
中引用该插件
....
plugins: ['@babel/plugin-syntax-dynamic-import']
....
2. 添加魔法注释
function getComponent() {return import(/* webpackChunkName:"lodash" */'lodash').then(({default: _}) => {var element = document.createElement('div');
element.innerHTML = _.join(['Clear', 'love'], '');
return element;
})
}
getComponent().then(element => {document.body.appendChild(element);
})
关键是注释:webpackChunkName: "lodash".
。打包后的文件名为 vendors~lodash.js
。
若想打包过后的文件名不带 vendors~
前缀,可以修改 webpack.common.js
中optimization
配置项:
....
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
venders: false,
default: false
}
}
}
....
splitChunks 配置详解
splitChunks: {
chunks: 'async', // all async initial 是否对异步代码进行的代码分割
minSize: 30000, // 引入模块大于 30kb 才进行代码分割
maxSize: 0, // 引入模块大于 Xkb 时,尝试对引入模块二次拆分引入
minChunks: 1, // 引入模块至被使用 X 次后才进行代码分割
maxAsyncRequests: 5, //
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {test: /[\\/]node_modules[\\/]/,
priority: -10, // 优先级
filename: 'vendors.js' // 打包文件名称
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true // 是否复用已打包代码
}
}
}
}
Lazy Loading && Chunk
import 动态引入组件
function getComponent() {return import(/* webpackChunkName:"lodash" */'lodash').then(({default: _}) => {var element = document.createElement('div');
element.innerHTML = _.join(['Clear', 'love'], '');
return element;
})
}
document.addEventListener('click', () => {
/* 当点击时才加载 lodash */
getComponent().then(element => {document.body.addChild(element);
})
})
页面初始化时,不会加载 lodash
。当点击页面时才加载。import
引入动态组件实现的 Lazy Loading
,其实跟Webpack
没什么关系。import
是 ES6
的语法标准。而 Webpack
借助 babel-profill
能识别该语法。
Chunk
每个打包的 js
文件都是一个chunk
打包分析 && Preloading && Prefetching
借助 webpack analyse
进行打包分析
在 package.json
的scripts
项中进行配置:
....
scripts: {"dev-build": "webpack --profile --json > stats.json --config ./build/webpack.dev.js"}
....
打包后会生成stats.json
,然后上传该文件至 webpack/analyse 进行分析
其他分析分工具
- webpack-chart
- webpack-bundle-analyzer
利用魔法注释实现Preload/Prefetch
document.addEventListener('click', () => {import(/* webpackPrefetch: true */ 'lodash').then(() => {....})
})
Preload
和 Prefetch
的区别:
-
preloaded chunk
与主模块并行加载,而prefetched chunk
是主模块加载完后再加载 -
preloaded chunk
具有中等优先级,可以立即下载。而prefetched chunk
是在浏览器空闲时下载。 - 浏览器支持程度不同
具体可以参考 prefetching/preloading-modules
CSS 文件的代码分割
若没有进行 css
的代码分割,通过 import
方式引入的样式文件,将会被当作普通的模块打包到 .js
文件中。
若需要对 css
进行代码分割,需要借助 optimize-css-assets-webpack-plugin
插件实现,具体如下:
// webpack.prod.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
....
module: [{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loder: 'css-loader',
options: {importLoaders: 2}
},
'saas-loader',
'postcss-loaerd'
]
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
}],
plugins: [
new MiniCssExtractPlugin({filename: '[name].css', // 入口文件中直接引入 css 匹配该规则
chunkFilename: '[name].chunk.css' // 非入口文件中引入或嵌套引入匹配匹配该规则
})
]
....
若需要对引入 css
进行合并、压缩,可以借助optimize-css-assets-webpack-plugin
。,具体配置如下:
// webpack.prod.js
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin")
....
optimization: {minimizer: [new OptimizeCSSAssetsPlugin({})]
}
....
若之前有配置过tree shaking
,则需要对以下文件进行修改:
webpack.common.js
optimization: {usedExports: true}
package.json
"sideEffects": ["*.css"]
Webpack 与浏览器缓存
webpack
实现浏览器缓存,主要是借助配置 output
中的 contenthash
来实现的。
// webpack.prod.js
....
output: {filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js'
}
....
旧版 webpack
进行打包时,虽然文件没有进行任何修改,但打包后生成的 contenthash
还是会改变,这时需要再进行一些配置。
// webpack.common.js
....
optimization: {
runtimeChunk: {name: 'runtime'}
}
....
构建性能
常规
保持版本最新
使用最新稳定版本的 webpack
、node
、npm
等,较新的版本更够建立更高效的模块树以及提高解析速度。
loaders
将 loaders
应用于最少数的必要模块中,而不是:
// webpack.common.js
module: {
rules: [
{
test: /\.jsx?$/,
use: ['babel-loader']
}
]
}
使用 include
字段仅将 loader
模块应用在实际需要用其转换的位置:
// webpack.common.js
module: {
rules: [
{
test: /\.jsx?$/,
include: path.resolve(__dirname, '../src'),
use: ['babel-loader']
}
]
}
Smaller = false
减少编译的整体大小,以提高构建性能。尽量保持 chunks
小巧。
- 使用更小 / 更少的库
- 在多页面应用程序中使用
CommonsChunksPlugin
- 在多页面应用程序中以
async
模式使用CommonsChunksPlugin
- 移除不需要的代码
- 只编译你在开发的代码
Worker Pool
thread-loader
可以将非常耗性能的 loaders 转存到 worker pool
中。<br/>
不要使用太多的 workers,因为 Node.js 的 runtime 和 loader 有一定的启动开销。最小化 workers 和主进程间的模块传输。进程间通讯 (IPC) 是非常消耗资源的。
持久化缓存
对于一些性能开销较大的 loader
之前可以添加cache-loader
,启用持久化缓存。
使用 package.json
中的 postinstall
清楚缓存目录。
Dlls
使用 DllPlugin
将更新不频繁的代码进行单独编译。这将改善引用程序的编译速度。即使它增加了构建过程的复杂度。
解析(resolve)
以下几步可以提高解析速度:
- 尽量减少
resolve.modules
、resolve.extensions
、resolve.mainFiles
、resolve.desciriptionsFiles
中类目的数量,因为它们会增加文件系统的调用次数。 - 如果你不使用
symlinks
,可以设置resolve.symlinks: false
- 如果你使用自定义解析
plugins
,并且没有指定context
信息,可以设置resolve.cacheWithContext: false
Development
在内存中编译
以下几个实用的工具通过在内存中进行代码的编译和资源的提供,但并不写入磁盘来提高性能:
webpack-dev-server
webpack-hot-middleware
webpack-dev-middleware
Devtool
需要注意在不同的 devtool
的设置,会导致不同的性能差异。
-
eval
具有最好的性能,但不能帮你转义代码 - 如果你能接受稍微差一些的 mapping 质量,你可以使用
cheap-source-map
选择来提高性能 - 使用
eval-source-map
配置进行增量编译
在大多数情况下,cheap-module-eval-source-map
是最好的选择。
避免在生产环境在才会用到的工具
某些实用工具,plugins
和 loaders
都只能在构建生产环境时才使用。例如,在开发时使用 UglifyJsPlugin
来压缩和修改代码是没有意义的。以下这些工具在开发中通常被排除在外:
UglifyJsPlugin
ExtractTextPlugin
[hash]/[chunkhash]
AggressiveSplittingPlugin
AggressiveMergingPlugin
ModuleConcatenationPlugin
最小化入口 chunk
webpack
只会在文件系统中生成已更新的 chunk
。应当在生成入口chunk
时,尽量减少入口 chunk
的体积,以提高性能。
Production
不要为了非常小的性能增益,牺牲了你应用程序的质量!!请注意,在大多数情况下优化代码质量,比构建性能更重要。
多个编译时
当进行多个编译时,以下工具可以帮助到你:
-
parallel-webpack
: 它允许编译工作在woker
池中进行。 -
cache-loader
: 缓存可以在多个编译之间共享。
工具相关问题
Babel
项目中的 preset/plugins
数量最小化
TypeScript
- 在单独的进程中使用
fork-ts-checker-webpack-plugin
进行类型检查 - 配置
loaders
时跳过类型检查 - 使用
ts-loader
时,设置happyPackMode: true
以及transpileOnly: true
Saas
node-sass
中有个来自 Node.js
线程池的阻塞线程的 bug。当使用 thread-loader
时,需要设置workParallelJobs: 2
其他
修改 CleanWebpackPlugin
根路径
在 webpack.config.js
配置中,需要对 plugins
中的 CleanWebpackPlugin
的根路径进行修改,可以通过配置 root
参数。
....
plugins: [new CleanWebpackPlugin(['dist'], {root: path.resolve(__dirname, '../')
})
]
....