尽量上新版本
使用最新 Webpack 版本是性价比最高的优化手段之一!新版本通常还会引入更多性能工具,例如 Webpack5 的 (持久化缓存)、(按需编译)。
lazyCompilation
vite等noBundle打包工具开发体验之所以快,很大一部分原因是在于他们模块加载策略:
Vite启动的时候没有编译全部,不像webpack从entry进去每一个依赖的模块都编译一遍。这就可以说是按需求编译的,因为浏览器支持esm,所以不需要讲这些js文件合并成一个文件,也就不用都编译一遍了。
⚡️初识Vite: 基本使用
现在webpack中也有“类似”功能了:
module.exports = {
// ...
experiments: {
lazyCompilation: true,
},
};
代码中通过异步引用语句如 import(‘./xxx’) 导入的模块(以及未被访问到的 entry)都不会被立即编译,而是直到页面正式请求该模块资源(例如切换到该路由)时才开始构建。
当然这功能还是实验阶段,更多参考配置在这里:
开发模式禁用产物优化
webpack 提供的许多产物优化功能有:Tree-Shaking、SplitChunks、Minimizer 等。
这些能力能够有效减少最终产物的尺寸,提升生产环境下的运行性能,但这些优化在开发环境中意义不大,反而会增加构建器的负担(都是性能大户)。
因此,开发模式下建议关闭这一类优化功能,具体措施:
- 关闭默认优化策略:mode=’development’ 或 mode = ‘none’,;
- 关闭代码压缩:optimization.minimize 保持默认值或 false,;
- 关闭模块合并:optimization.concatenateModules 保持默认值或 false,scopeHosting;
- 关闭代码分包:optimization.splitChunks 保持默认值或 false;
关闭 Tree-shaking:optimization.usedExports 保持默认值或 false;
module.exports = {
// ...
mode: "development",
optimization: {
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false,
minimize: false,
concatenateModules: false,
usedExports: false,
},
};
不要滥用source-map
source-map 是一种将经过编译、压缩、混淆的代码映射回源码的技术。不过,source-map 操作本身也有很大构建性能开销。
webpack 提供了 devtool 选项(,这里有详细的对比),可以配置 eval、source-map、cheap-source-map 等值,推荐实践方案:开发环境使用 eval ,确保最佳编译速度;
- 生产环境使用 source-map,获取最高质量。
动态加载
webpack实现了import动态加载的语法。
比如上述代码中,doAsync代码如果比较庞大,其实没有必要出现在主页面的代码中,因为不一定会被点击,不一定用到,所以使用import() 异步加载语法。document.getElementById("btn").addEventListener("click", async () => {
const dosth = await import("./doAsync");
dosth();
});
动态加载的组件会作为新的chunk被分包。
不过值得注意的是:
- 动态加载也不能滥用,用的太多是必导致文件过于琐碎,从而使得 HTTP 通讯次数也会变多,在 HTTP 1.x 环境下这可能反而会降低网络性能,得不偿失;
- 动态加载将会引入一段2.5kb的runtime代码支持,所以,当你异步的代码小于2.5kb,很明显不划算;
多数情况下我们没必要为小模块使用动态加载能力 目前社区比较常见的用法是配合 SPA 的前端路由能力实现页面级别的动态加载
import { createRouter, createWebHashHistory } from "vue-router";
const Home = () => import("./Home.vue");
const Foo = () => import(/* webpackChunkName: "sub-pages" */ "./Foo.vue");
const Bar = () => import(/* webpackChunkName: "sub-pages" */ "./Bar.vue");
// 基础页面
const routes = [
{ path: "/bar", name: "Bar", component: Bar },
{ path: "/foo", name: "Foo", component: Foo },
{ path: "/", name: "Home", component: Home },
];
const router = createRouter({
history: createWebHashHistory(),
routes,
});
export default router;
上面demo中,Home、Foo、Bar 三个组件均动态加载导入,这样当页面切换到相应路由时才会加载对应组件代码。
外置依赖
externals 的主要作用是将部分模块排除在 Webpack 打包系统之外。什么意思呢,就是说,用externals声明了的依赖,webpack会默认已经存在在系统中了,比如在html中用script标签从CDN资源引入,这样就不用webpack再重复打包了。
<script defer crossorigin src="//unpkg.com/react@18/umd/react.development.js"></script>
externals: {
react: "React",
},
虽然结果上看浏览器还是得消耗这部分流量,但结合 CDN 系统特性:
- 能够就近获取资源,缩短网络通讯链路;
- 能够将资源分发任务前置到节点服务器,减轻原服务器 QPS 负担;
- 用户访问不同站点能共享同一份 CDN 资源副本。所以网络性能效果往往会比重复打包好很多。
HTTP 缓存优化
关于http缓存相关的有2点:
- 适当的文件拆分策略:比如node_modules里面的内容几乎不变,就可以作为独立的文件来从整体代码中拆出;
- 文件命名策略,为文件设置hash占位符;
hash占位符可以设置为:
- [fullhash]:整个项目的内容 Hash 值,项目中任意模块变化都会产生新的 fullhash;
- [chunkhash]:产物对应 Chunk 的 Hash,Chunk 中任意模块变化都会产生新的 chunkhash;
- [contenthash]:内容 Hash 值,仅当产物内容发生变化时才会产生新的 contenthash,实用性较高。
当内容没有变化,contenthash的计算值不会变化,故而文件名不变,请求文件时能命中缓存,产物文件不会被重复下载,一直到文件内容发生变化。module.exports = {
// ...
entry: { index: "./src/index.js", foo: "./src/foo.js" },
output: {
filename: "[name]-[contenthash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [new MiniCssExtractPlugin({ filename: "[name]-[contenthash].css" })],
};
这里有一个问题:异步模块的内容变了,主文件的内容一定变化。这个值得注意,但是很好理解,因为主文件中会依赖异步文件名称。(异步文件内容变化导致异步文件名称变化,进而导致主文件内容变化,导致主文件名称变化)Tree Shaking
tree shaking :在 Rollup 中率先实现,Webpack 自 2.0 版本开始接入,是一种基于 ES Module 规范的 Dead Code Elimination 技术。 它会在运行过程中静态分析模块之间的导入导出,判断哪些模块导出值没有被其它模块使用 —— 相当于模块层面的 Dead Code,并将其删除。
启动 Tree Shaking 功能必须同时满足两个条件:
- 配置 optimization.usedExports 为 true(标记模块导入导出列表);
- 启动代码优化功能,以下都可以:
- 配置 mode = production
- 配置 optimization.minimize = true
提供 optimization.minimizer 数组
// 满足2个条件
// 开启tree shaking
module.exports = {
mode: "production", // 满足条件2
optimization: {
usedExports: true, // 满足条件1
},
};
Scope Hoisting
scope hoisting : 将符合条件的多个模块合并到同一个函数空间 中,从而减少产物体积,优化性能。
提供了三种开启 Scope Hoisting 的方法:使用 mode = ‘production’ 开启生产模式;
- 使用 optimization.concatenateModules 配置项;
- 直接使用 ModuleConcatenationPlugin 插件;
上述三种方法本质都会调用 ModuleConcatenationPlugin 完成模块分析与合并操作。
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
// 三种方法都能开启scope hoisting
module.exports = {
// 方法1: 将 `mode` 设置为 production,即可开启
mode: "production",
// 方法2: 将 `optimization.concatenateModules` 设置为 true
optimization: {
concatenateModules: true,
usedExports: true,
providedExports: true,
},
// 方法3: 直接使用 `ModuleConcatenationPlugin` 插件
plugins: [new ModuleConcatenationPlugin()]
};
tree shaking 、scope hoisting 底层基于 ES Module 方案的静态特性,推断模块之间的依赖关系,因此注意在非ESM的场景下会失效。
优化 eslint 性能
使用新版本组件 eslint-webpack-plugin 替代旧版 eslint-loader(eslint-webpack-plugin 在模块构建完毕后执行检查,不会阻断文件加载流程,性能更优)
或者其他使用eslint的方式:
- 编辑器插件完成 ESLint 检查、错误提示、自动 Fix
- 使用 husky,仅在代码提交前执行 ESLint 代码检查;
监控产物体积
Webpack 专门为此提供了一套 性能监控方案,当构建生成的产物体积超过阈值时抛出异常警告,以此帮助我们时刻关注资源体积,避免因项目迭代增长带来过大的网络传输,用法:
module.exports = {
// ...
performance: {
// 设置所有产物体积阈值
// 若此时产物体积超过 170KB,则报错
maxAssetSize: 170 * 1024,
// 设置 entry 产物体积阈值
maxEntrypointSize: 244 * 1024,
// 报错方式,支持 `error` | `warning` | false
hints: "error",
// 过滤需要监控的文件类型
assetFilter: function (assetFilename) {
return assetFilename.endsWith(".js");
},
},
};
最大产物体积的大小多少合适?经验规则是170kb,参考: