性能优化, 可以分为三个方面:
- 构建性能
主要指开发阶段的构建性能, 降低从打包开始到代码呈现效果所经过的时间 - 传输性能
指打包后的js代码传输到浏览器经过的时间. 优化传输性能要考虑到
1. 总传输量
2. 文件数量
3. 浏览器缓存 - 运行性能
指js代码在浏览器端的运行速度, 主要取决于如何书写高性能代码
构建性能
1. 减少模块解析
模块解析包括: 抽象语法树分析, 依赖分析, 替换依赖函数等.
哪些模块不需要解析?
- 模块中无其他依赖: 一些已经打包好的第三方库, 比如jquery
如何让webpack不解析某些模块?
- 配置noParse
2. 优化loader性能
- 进一步限制loader的应用范围
使用exlucde
或include
排除或仅包含需要应用loader的场景(在每个module下, 每个rule配置对象内) - 缓存loader结果
使用cache-loader
缓存loader(它必须放在所有loader第一位)
loader是倒序运行的, cache-loader放第一位却能控制其他loader是否执行, 是因为其实loader运行还包括一个过程, 即pitch. pitch是loader函数的静态属性, 它也是一个函数, 这个函数有一个参数是文件的路径, 如果这个函数返回文件内容, 那么直接交给到配置里它的前一个loader去执行, 如果pitch函数没返回, 那么把路径按照配置的顺序交到下一个loader的pitch函数. 直到最后一个loader, pitch函数都没返回, 才读取文件内容, 然后开始倒序交给一个一个loader处理. - 为loader的运行开启多线程
thread-load可以开启一个线程池, 后续的loader会放到线程池中运行.但是后续的loader不能:
- 使用webpack api生成文件
- 使用自定义的plugin api
- 访问webpack options
3. 热替换
热替换并不能降低构建时间, 但是能降低代码改动到效果呈现的时间.
a) 更改配置
module.exports = {
devServer: {
hot: true
},
plugins: [
// 可选, webpack 4之后如果设置了hot: true, 默认使用这个插件
new webpack.HotModuleReplacementPlugin()
]
}
b) 更改代码
if (module.hot) {
module.hot.accept();
}
样式热替换: 使用style-loader可以进行热替换. 但是如果用mini-css-extract-plugin, 由于它生成文件是在构建期间, 运行期间无法改动文件, 热替换对它无效
传输性能
1. 分包
什么是分包?
将一个整体的代码, 分布到不同的打包文件中
为什么要分包?
- 减少公共代码, 降低总体积(特别是一些大型的第三方库)
- 充分利用浏览器缓存
什么时候要分包?
- 多个chunk引入了公共模块
- 公共模块体积较大或较少变动
如何分包?
-
手动分包
步骤:- 打包公共模块, 生成manifest.json资源清单
- 使用公共模块, 此时代码中引入公共模块的时候, 就不再把公共模块代码引入打包
打包公共模块是另外的过程, 需要单独使用另一个配置文件, 专门配置, 如:
const webpack = require("webpack");
const path = require("path");
module.exports = {
mode: "production",
entry: {
jquery: ["jquery"],
lodash: ["lodash"]
},
output: {
filename: "dll/[name].js",
// 打包结果暴露的全局变量名
library: "[name]"
},
plugins: [
new webpack.DllPlugin({
path: path.resolve(__dirname, "dll", "[name].manifest.json"),
name: "[name]"
})
]
}
然后要在源代码的配置文件中:
- 重新设置clean-webpack-plugin, 避免它清除公共模块
- 在模板html中手动引入公共模块
- 使用DllReferencePlugin, 如
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"]
}),
new HtmlWebpackPlugin({
template: "./public/index.html"
}),
new webpack.DllReferencePlugin({
manifest: require("./dll/jquery.manifest.json")
}),
new webpack.DllReferencePlugin({
manifest: require("./dll/lodash.manifest.json")
}),
]
- 自动分包
自动分包现在由webpack根据分包策略配置自动完成. webpack有默认的分包配置, 能够在绝大多数情况下都适用.
详细配置见官网, 大致配置如下:
module.exports = {
optimization: {
// 分包策略的配置
splitChunks: {
...
}
}
}
2. 代码压缩
webpack自动集成了terser(一个代码压缩工具)
在生产环境下, webpack使用 terser自动完成压缩.
3. tree shaking
能够使得单模块体积优化. 可以移除模块之间的无效代码. (某些模块的导出的代码不一定用到, tree shaking用于移除掉不会用到的部分)
webpack 2开始就支持了tree shaking, 只要是生产环境, tree shaking自动开启.
原理
webpack会从入口模块出发寻找依赖关系.
当解析一个模块时, webpack会根据es6的模块导入语句来判断, 该模块依赖了另一个模块的哪个导出.
webpack之所以选择es6的模块导入语句, 是因为es6模块有以下特点:
- 导入导出语句只能是顶层语句
- import的模块名只能是字符串常量
- import绑定的常量是不可变的
这些特征都非常有利于分析出稳定的依赖.
如果依赖的是一个导出的对象, 由于js语言动态的特性, 为了保证代码正常运行, 它不会移除对象中的任何信息.
因此, 我们在编写代码的时候, 尽量:
- 使用export xxx导出, 而不是export default {xxx}导出
- 使用import {xxx} from "xxx"导入或import * as xxx from "xxx"导入, 而不使用import xxx from "xxx"导入
依赖分析完毕后, webpack会根据每个模块每个导出是否被使用, 标记其他导出为dead code, 然后交给代码压缩工具移除dead code.
4. 懒加载
懒加载指在运行时再动态加载需要的文件.
直接看示例代码
const btn = document.querySelector("button");
btn.onclick = async function () {
// 动态加载
// import(xxx)是es6的草案, 不是正式标准
// 此处可以想象为浏览器使用jsonp的方式远程去读取一个js模块
// 这里被import的内容会被单独打包成一个js
// import()会返回一个promise, 结果类似于(* as obj)
const _ = await import(/* webpackChunkName: "lodash */"lodash-es");
const result = _.chunk([3, 5, 6, 89, 1, 31], 2);
console.log(result);
}
5. gzip
客户端 - -服务器传输之间, 有可能会对传输内容进行压缩. 浏览器发送请求时可以设置Accept-Encoding
来告诉服务器支持哪些压缩方式, 服务器就可以返回压缩后的资源文件给客户端.
webpack可以使用compression-webpack-plugin
来对打包内容进行压缩(增加输出压缩后的文件, 而不是只输出压缩后的文件)