性能分析
1. 统计基本信息
使用webpack内置的stats
可以统计出构建时间、构建资源清单及资源大小等信息
使用方法:
1. cli
webpack --env production --json > stats.json
2. node API
webpack(config, (err, stats) => {
console.log(stats);
});
2. 速度分析
使用speek-measure-webpack-plugin
插件功能
- 分析出整个构建时间和每个loader和plugin的构建时间
- 时间过长的标红,较长的标黄
插件使用:包裹webpack的配置
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp({
// webpack配置
});
3. 体积分析
使用webpack-bundle-analyzer
以可视化形式展示打包依赖模块的体积。
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
};
构建完成后会启动本地服务,serve 8888端口,浏览器中访问就能看到分析结果。
提升构建速度
使用高版本的webpack和nodejs
高版本的webpack和nodejs构建速度更快
webpack4优化原因:
- v8带来的优化(for of 代替forEach,Map、Set代替Object、includes代替indexOf)
- 默认使用更快的md4 hash算法
- webpack AST可以直接从loader传递给AST,减少解析时间
- 使用字符串方法代替正则
多进程多实例构建
资源并行解析可选方案
- HappyPack
作者已经不维护,建议使用webpack官方提供的"thread-loader" - thread-loader
{
test: /.js$/,
user: [
{
loader: 'thread-loader',
options: {
workers: 3
}
},
'babel-loader'
]
}
- parallel-webpack
多进程多实例并行压缩
1. 方法一,使用parallel-uglify-plugin插件
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
plugins: [
new ParallelUglifyPlugin({
// ...
})
]
};
2. 方法二,使用uglifyjf-webpack-plugin
目前webpack官方推荐使用terser-webpack-plugin
3. 方法三,使用terser-webpack-plugin,开启parallel参数
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: 4
})
]
}
};
进一步分包:预编译资源模块
1. 使用html-webpack-externals-plugin
将react、react-dom基础包通过cdn引入,不打入bundle中
使用方法:
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: '//path/to/your/cdn-domain/react.min.js',
global: 'React'
},
...
]
});
]
效果
<script type="text/javascript" src="//path/to/your/cdn-domain/react.min.js"></script>
2. 预编译资源模块,使用DLLPlugin和DllReferencePlugin
通常来说,我们的代码都可以至少简单区分成业务代码和第三方库。如果不做处理,每次构建时都需要把所有的代码重新构建一次,耗费大量的时间。然后大部分情况下,很多第三方库的代码并不会发生变更(除非是版本升级),这时就可以用到dll:把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码。
使用dll时,构建过程分成dll构建过程和主构建过程,所以需要两个构建配置文件,例如叫做webpack.config.js
和webpack.dll.config.js
。
步骤:
- 使用DLLPlugin进行分包,对第三方包打包,完成后打包结果保存在项目中,后面就不需要再构建第三方包了。
- 每次构建业务项目时候,使用DllReferencePlugin实现对构建好的第三方包dll的解析和处理。
示例:
使用DLLPlugin进行分包
// webpack.dll.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
context: process.cwd(),
entry: {
library: [
'react',
'react-dom',
'redux'
]
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, './build/library'),
library: '[name]'
},
plugins: [
new webpack.DllPlugin({
name: '[name]',
path: './build/library/[name].json'
})
]
};
使用DllReferencePlugin对manifest.json引用
// webpack.config.js
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
manifest: require('./build/library/manifest.json')
})
]
};
引用效果
<script src="/build/library/library.dll.js"></script>
原理
使用DLLPlugin对第三方库打包时候,会生成打包结果和manifest.json文件到指定目录,文件中包含各第三方包的引用关系等信息。
使用DllReferencePlugin插件打包业务代码时候,我们通过配置告诉插件DLLPlugin打包的产物的目录,DllReferencePlugin会分析manifest文件,DLL的包不会参与打包构建过程,并且还生成相关的引用。
3. 使用externals选项
使用externals选项可以排除指定的第三方模块,在构建过程中忽略它们。
使用方法示例:
首先在html中引入第三方模块
<script src="https://example-cdn.com/react.min.js"></script>
然后在externals选项里面配置要排除的模块和引用方式
// webpack.config.js
module.exports = {
externals: {
'react': 'React'
}
};
在项目中引用
import React from 'react';
充分利用缓存提升二次构建速度
缓存思路
- babel-loader开启缓存
- terser-webpack-plugin开启缓存
- 使用cache-loader或者hard-source-webpack-plugin
缩小构建目标
目的:尽可能少构建模块
比如babel-loader不解析node_modules
module.exports = {
rules: {
test: /.js$/,
loader: 'happypack-loader',
exclude: 'node_moudles'
}
};
减少文件搜索范围
- 优化resolve.modules配置(减少模块搜索层级)
- 优化resolve.mainFields配置(缩小模块入口搜索范围)
- 优化resolve.extensions配置(比如限定.js,其他引用时候补全后缀)
- 合理使用alias(缩小模块引用路径搜索范围)
使用oneOf
通常来讲,同一种类型的文件只能由一个loader处理,那么正常来讲的逻辑应该是,比如我是一个css文件,那么我匹配到test为css后缀的loader我就应该立即执行了,但是事实是,虽然匹配到了,但是还是会遍历完整一遍再进行解析,这样来讲效果明显就更低了。
而Oneof语法就是解决这个问题的,使文件一旦匹配上loader之后就立即解析,省去了全盘遍历这个不必要的过程。
如果对于需要多个loader共同解决的文件类型,比如js。那就需要把其中的loader放历Oneof之外,这样才能实现loader也能同时执行到。
提升加载和运行速度
使用Tree Shaking擦除无用的js和css
摇树js
Tree-shaking的本质是消除没有用到的代码。主要的效果是,引用了但没有使用的模块,不会被打包到最终的bundle中。
Tree-shaking要求模块是ESM,ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。
所谓静态分析就是不运行代码,从语法上对代码进行分析,ES6之前的模块化规范,比如我们可以动态require一个模块,只有执行后才知道是否要引用某个模块,引用的是什么模块(因为require是运行时调用,所以require理论上可以运用在代码的任何地方,而且require支持传入表达式作为参数,只能在运行时才知道引入的是哪个模块),这个就不能通过静态分析去做优化。
webpack默认支持Tree-shaking,如果mode为"production"webpack在构建会做Tree-shaking的操作。
摇树css
摇树css的基本思路是,给定content和css,分析content中用到的选择器,然后分析css文件中没有用到的选择器,将其移除。
摇树css工具有:
- PurifyCSS
使用purgecss-webpack-plugin
- uncss
scope-hoisting
scope hoisting 是 webpack3 的新功能,直译过来就是「作用域提升」。熟悉 JavaScript 都应该知道「函数提升」和「变量提升」,JavaScript 会把函数和变量声明提升到当前作用域的顶部。「作用域提升」也类似于此,webpack 会把引入的 js 文件“提升到”它的引入者顶部。
在之前版本中,webpack打包会把每个模块用闭包封装,通过webpack_require 引用。
这样会存在问题:
- ⼤量作用域包裹代码,导致体积增大(模块越多越明显)
- 运行代码时创建的函数作⽤域变多(每个模块引用都要创建一个函数作用域),内存开销变大
为了解决这两个问题,webpack启用scope hoisting,将每个模块都提升到引入者顶部,这样模块不会因为依赖链路较深而导致调用栈变深,它们都在同一层。
这样就解决了上面的两个问题:
- 代码量明显减少。
- 调用栈变浅,减少了创建作用域的内存和计算损耗,提升了运行速度。
使用webpack进行图片压缩
图片压缩实际是使用了基于node库的imagemin或者tinypng API
在webpack中配置image-webpack-loader
,这个loader实际是使用了imagemin进行图片压缩
优化polyfill方案
polyfill的方案:
1. babel-polyfill
将babel-polyfill作为一个单独的入口打包
这样做的一个问题是会将所有polyfill代码都打包进去(200k左右),导致代码体积过大
const path = require('path');
module.exports = {
entry: [
'babel-polyfill',
path.resolve(__dirname, './src/index.js')
]
};
2. babel-presets的选项中“useBuiltIns”
选项值为“false”时候,不加入polyfill。
选项值为“entry”时候,将所有polyfill打包进项目。
选项值为“usage”时候,按需加载,并且做了优化:将polyfill的工具方法提取成公共资源,而不会每个 polyfill代码都内联相同的工具方法 。
此外babel-presets中还支持根据支持的浏览器来选择polyfill,这通过"target"属性配置。
// .babelrc
{
"presets": [
"@babel/preset-env",
{
"useBuildIns": "useage"
}
]
}
3. @babel/runtime和@babel/plugin-transform-runtime
@babel/runtime
实现polyfill的功能,它分析代码,然后添加相关的polyfill,即实现了polyfill的按需加载。它和上述两种方法的区别是,它在添加polyfill代码时候,不会污染全局变量,而是定义局部方法来实现polyfill。因为这个特点,它更适合用在第三方库中,而上面两种适合用在业务代码项目中。
其缺点在于不支持实例方法的polyfill,如arr.includes(1);
。
由于 @babel/runtime
也是使用内联代码实现polyfill,因此可能多个文件中会内联相同的工具方法。@babel/plugin-transform-runtime
用来解决这个问题,它提取公共的工具方法,每个文件使用时候引入相关的工具方法,这样减少的代码体积。
4. polyfill-service,使用动态polyfill服务
根据浏览器userAgent选择相应的polyfill,有些浏览器支持的,就不再下发冗余polyfill。
可以使用官方的服务。
或者自建polyfill服务。
可能存在的问题:浏览器ua不准,有些国内浏览器修改ua导致polyfill判断错误,降级方案是下载所有polyfill
https://cdn.polyfill.io/v2/polyfill.min.js
使用prerender-spa-plugin预渲染
prerender-spa-plugin插件启用无头浏览器,加载项目的路由,并渲染出首屏页面(也可以配置其他路由),然后生成静态页面,保存在指定的目录。
我们的静态资源服务器就可以serve预渲染的页面了。
使用这个插件相当于在构建阶段就渲染好了首屏页面,极大地提升了首屏性能。