webpack优化方案 | 实践总结
本文 demo 地址
本文项目代码位置: 源码地址
为什么需要构建工具
webpack 能干什么?
- 开发时,启动本地服务
- 解决 js、css 的依赖问题。(以前经常因为引入顺序问题,导致 css 没起作用或某个js变量找不到)
- 将 ES6、vue/react、jsx 语法编译为浏览器可识别的代码
- 合并、压缩、优化打包后的体积
- css 前缀补齐/预处理器
- 使用 eslint 校验代码
- 单元测试
webpack 配置组成部分
module.exports = {
entry: '', // 指定入口文件
output: '', // 指定输出目录和输出文件名
mode: '', // 环境
module: {
rules: [ // loader配置
{test: '', use: ''}
]
},
plugins: [ // 插件配置
new xxxPlugin()
]
}
基础常用的 Loader
为什么需要 loader ?
webpack 原生只支持 js、json 两种模块类型,所以需要 loader 把其他类型的文件转化成有效的模块,并可以添加到依赖图中。
loader 本身是一个函数,接受源文件作为参数,返回转换的结果
功能 | loader | 说明 |
---|---|---|
解析es6 | babel-loader | 配合.babelrc使用 |
解析vue | vue-loader | |
解析css | css-loader | 用于加载.css文件,并转换成commonjs对象 |
style-loader | 将样式通过<style> 标签插入到head中 | |
解析less | less-loader | 将less转换成css |
解析图片和字体 | file-loader | 用于处理文件(图片、字体) |
url-loader | 也可以处理图片和字体,和file-loader功能类似,但它还可以设置较小资源自动转base64(内部用了file-loader),使用options:{limit: xxx} |
基础常用的 Plugin
插件用于 bundle 文件的优化、资源管理和环境变量注入,它作用于整个构建过程
- clean-webpack-plugin: 打包前自动清理 dist 目录
- html-webpack-plugin:自动生成 html,并将打包后的 js 插入进去
- mini-css-extract-plugin: 把 css 提取成单独的文件,与
style-loader
功能互斥,不能同时使用 - 关于代码压缩:
JS文件的压缩 :默认开启了内置的
terser-webpack-plugin
,webpack 在打包时会自动压缩 js 代码 css文件的压缩 :使用optimize-css-assets-webpack-plugin
,同时使用cssnano
(处理器)
plugins: [
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
})
]
html文件的压缩:修改 html-webpack-plugin
,设置压缩参数
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/index.html'),
filename: 'index.html',
chunks: ['main', 'other'], //要包含哪些chunk
inject: true, //将chunks自动注入html
minify: { // 压缩相关
html5: true,
collapseWhitespace: true, //压缩空白字符
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: true
}
})
]
webpack性能优化
开发环境性能优化
模块热替换(HotMoudleReplacement)
开启 dev-server
后默认只要有一个文件变化,会重新构建,刷新浏览器页面。
模块热替换: 只重新打包变更的模块,局部刷新,保留数据状态,(而不是将所有模块重新打包,刷新页面)提升构建速度,使开发更加方便
通过 devServer.hot
启用,其内部依赖 webpack.HotModuleReplacementPlugin
实现,HotModuleReplacementPlugin
会在 hot: true
时自动被引入,可以不写
- 样式文件:可以直接使用 HMR 功能,因为
style-loader
内部实现了 - js 文件:默认不能使用 HMR 功能,需要修改 js 代码,添加支持 HMR 功能的代码
if(module.hot){ // 如果开启了HMR功能
// 监听xxx.js文件的变化,一旦发生变化,其他模块不会重新打包,会执行回调函数
module.hot.accept('./xxx.js', function(){
fn()
})
}
- html文件:没有热替换,也没有热更新,热更新可以通过在入口文件添加 html 文件路径来打开,但通常没有必要
使用source-map
由于经过 webpack
打包后的代码是经过各种 loaders
,plugins
转换过后的一个大的js文件,开发过程中无法调试。source-map
是一种提供源代码到构建后代码映射的技术,报错时通过 source map
可以定位到源代码。
启用方式:
module.exports = {
devtool: 'source-map'
}
选项:
[inline- | hidden- | eval-] [nosources- ] [cheap- [module- ]]source-map
- source-map: 产生
.map
文件,提供错误代码准确信息和源代码的错误位置 - inline:内联,将
.map
作为DataURI
嵌入,不单独生成.map
文件,构建速度更快 - eval:内联,使用
eval
包裹模块代码,指定模块对应文件 - cheap:只精确到行,不精确到列
- module:包含
loader
的sourcemap
推荐组合:
-
开发环境:速度快,调试更友好
eval-source-map
(eval
速度最快,source-map
调试最友好) -
生产环境:内联会让代码体积变大,所以生产环境不用内联 1.考虑是否要隐藏源代码?
nosources-source-map
---全部隐藏hidden-source-map
---只隐藏源代码,会提示构建后代码错误信息 2.考虑是否要调试友好?source-map
生产环境性能优化
使用文件指纹进行版本控制和缓存
当设置了 http 强缓存,比如有效期为一天,如果不使用 hash,当这个文件改变了,因为文件名没变,所以客户端使用的还是旧的缓存;如果使用了 hash,这时文件名就改变了,就会请求新的资源,而没有更改过的文件继续使用缓存
- hash: 构建的
hash
,每次构建都会改变,不建议使用 - chunkhash: 和
webpack
打包的chunk
有关,不同的entry
会生成不同的chunkhash
值 - contenthash: 根据文件内容来定义hash,文件内容不变,则
contenthash
不变,推荐在css
文件上使用
js 文件的指纹设置:
//设置 output 的 filename,使用 [chunkhash]
module.exports = {
output: {
filename: '[name][chunkhash:8].js',
path:__dirname+'/dist'
}
}
css 文件的指纹设置:
使用 MiniCssExtractPlugin
将 css
从 js
中提出来,然后使用 [contenthash]
plugins: [
new MiniCssExtractPlugin({
filename: '[name][contenthash:8].css'
})
]
补充: module、chunk、bundle的区别
- module:模块,源代码中的一个文件就是一个模块
- chunk:一个入口文件所依赖的一大块就是一个
chunk
,可以理解为一个entry
对应一个chunk
- bundle:打包后的资源,一般来说一个
chunk
就对应一个bundle
,但也可以通过一些插件进行拆包,把一个大chunk
拆分为多个bundle
,比如MiniCssExtractPlugin
tree shaking
tree shaking
(摇树优化):一个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件都会被打到 bundle
里面去,tree shaking
就是只把用到的方法打入bundle
,没用到的方法会在 uglify
阶段被擦除掉。
使用:webpack
默认支持,在 .babelrc
里设置 module:false
即可
webpack
会在 production mode
的情况下默认开启 tree shaking
要求:必须是 es6 语法,cjs 的方式不支持
tree shaking 原理
DCE:永远不会被用到的代码,比如引入了一个方法但是没调用 或者 if(false){xxx}
利用 ES6 模块的特点:
- 只能作为模块顶层的语句出现
import
的模块名只能是字符串常量import binding
是immutable
的
在打包之前静态的分析文件,在uglify阶段删除无用代码
code split
将一个大bundle文件拆包,拆包的方案可以在cacheGroups里配置
- splitChunks
// splitChunks默认配置
optimization: {
splitChunks: {
chunks: 'all', // 无论同步引入还是异步引入
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/, // 匹配node_modules目录下的文件
priority: -10 // 优先级配置项
},
default: {
minChunks: 2, // 至少引用了2次
priority: -20, // 优先级配置项
reuseExistingChunk: true
}
}
}
}
在默认设置中
- 会将
node_mudules
文件夹中的模块打包进一个叫vendors
的bundle
中 - 所有引用超过两次的模块分配到
default bundle
中 ,可以通过priority
来设置优先级。
DLL
将第三方库和业务基础包单独打成一个文件,只在第一次打,或者需要更新依赖的时候打,此后每次就可以只打自己的源代码,加快了构建速度。
方法:使用 DLLPlugin
进行分包,DllReferencePlugin
对 manifest.json
引用
分包需要单独的配置文件:
// webpack.dll.js
module.exports={
entry: {
lib: [
'lodash',
'jquery'
]
},
output: {
filename: '[name]_[chunkhash].dll.js',
path: path.join(__dirname, 'build/lib'),
library: '[name]' // 打包后对外暴露的全局变量名称
},
plugins: [
new webpack.DllPlugin({
name: '[name]', // manifest.json中的name,要与ouput.library名一致
path: path.join(__dirname, 'build/lib/manifest.json'),
})
]
}
在 package.json
中添加命令对 dll
单独打包:
"scripts": {
"dll": "webpack --config webpack.dll.js"
},
使用 DllReferencePlugin
对 manifest.json
进行引用,告诉 webpack
使用了哪些动态链接库,不用再打包这里面的东西
// webpack.prod.js
new webpack.DllReferencePlugin({
manifest: require('./build/lib/manifest.json')
}),
使用 addAssetHtmlWebpackPlugin
将 dll
资源插到 html
里
// webpack.prod.js
new addAssetHtmlWebpackPlugin([
{
filepath: path.resolve(__dirname, './build/lib/*.dll.js'),
outputPath: 'static', // 将*.dll.js拷贝后的输出路径,相对于html文件
publicPath: 'static'
}
])
demo 地址: webpack实践-dll-plugin分支
使用前后对比:
使用 dllplugin
前,基础库打到了 main.js
里,占了 160kb
:
使用后:
main.js
只剩 1.23kb
splitChunks 和 dll 的区别
splitChunks
是在构建时拆包,dll
是提前构建好基础库,打包的时候就不需要打基础库了,时间上dll
比splitChunks
快一点dll
需要多配置一个webpack.dll.config.js
,而且一旦dll
中的依赖有更新,得走两遍打包,比splitChunks
麻烦一些- 推荐使用
splitChunks
去提取页面间的公共js
文件。DllPlugin
用于基础包(框架包、业务包)的分离。
多进程打包
使用 thread-loader
开启多进程打包,加快打包速度!
注意:启动进程需要大概 600ms
,进程间通信也有花销,项目小的话开启多进程得不偿失,所以只有当项目比较大,打包耗时较长的时候才适合使用多进程。
module: {
rules: [
{
test: /.js$/,
use: [
{
loader: 'thread-loader',
options: {
workers: 2 //开启两个进程
}
},
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
cacheDirectory: true
}
}
]
},
]
}
多页面打包通用配置
多页面打包需要多个入口文件,多个 HtmlWebpackPlugin
产生多个 html
。我们不可能去手写很多个入口和 HtmlWebpackPlugin
方案: 动态获取 entry
和设置 html-webpack-plugin
数量
// 核心方法
const setMPA = () => {
const entry = {};
const htmlWebpackPlugins = [];
const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'));
entryFiles.forEach(entryFile => {
const match = entryFile.match(/src\/(.*)\/index\.js/);
const pageName = match && match[1];
entry[pageName] = entryFile;
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: [pageName], //要包含哪些chunk
inject: true, //将chunks自动注入html
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
}),
)
})
return {
entry,
htmlWebpackPlugins
}
}
其他具体配置见 webpack实践-mpa-build分支
转载自:https://juejin.cn/post/6880487034130169869