尽量上新版本

使用最新 Webpack 版本是性价比最高的优化手段之一!新版本通常还会引入更多性能工具,例如 Webpack5 的 (持久化缓存)、(按需编译)。

lazyCompilation

vite等noBundle打包工具开发体验之所以快,很大一部分原因是在于他们模块加载策略:
image.png

Vite启动的时候没有编译全部,不像webpack从entry进去每一个依赖的模块都编译一遍。这就可以说是按需求编译的,因为浏览器支持esm,所以不需要讲这些js文件合并成一个文件,也就不用都编译一遍了。

⚡️初识Vite: 基本使用
现在webpack中也有“类似”功能了:

  1. module.exports = {
  2. // ...
  3. experiments: {
  4. lazyCompilation: true,
  5. },
  6. };

代码中通过异步引用语句如 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;

    1. module.exports = {
    2. // ...
    3. mode: "development",
    4. optimization: {
    5. removeAvailableModules: false,
    6. removeEmptyChunks: false,
    7. splitChunks: false,
    8. minimize: false,
    9. concatenateModules: false,
    10. usedExports: false,
    11. },
    12. };

    不要滥用source-map

    source-map 是一种将经过编译、压缩、混淆的代码映射回源码的技术。不过,source-map 操作本身也有很大构建性能开销。
    webpack 提供了 devtool 选项(,这里有详细的对比),可以配置 eval、source-map、cheap-source-map 等值,推荐实践方案:

  • 开发环境使用 eval ,确保最佳编译速度;

  • 生产环境使用 source-map,获取最高质量。

    动态加载

    webpack实现了import动态加载的语法。
    1. document.getElementById("btn").addEventListener("click", async () => {
    2. const dosth = await import("./doAsync");
    3. dosth();
    4. });
    比如上述代码中,doAsync代码如果比较庞大,其实没有必要出现在主页面的代码中,因为不一定会被点击,不一定用到,所以使用import() 异步加载语法。
    动态加载的组件会作为新的chunk被分包。
    不过值得注意的是:
  1. 动态加载也不能滥用,用的太多是必导致文件过于琐碎,从而使得 HTTP 通讯次数也会变多,在 HTTP 1.x 环境下这可能反而会降低网络性能,得不偿失;
  2. 动态加载将会引入一段2.5kb的runtime代码支持,所以,当你异步的代码小于2.5kb,很明显不划算;

    多数情况下我们没必要为小模块使用动态加载能力 目前社区比较常见的用法是配合 SPA 的前端路由能力实现页面级别的动态加载

  1. import { createRouter, createWebHashHistory } from "vue-router";
  2. const Home = () => import("./Home.vue");
  3. const Foo = () => import(/* webpackChunkName: "sub-pages" */ "./Foo.vue");
  4. const Bar = () => import(/* webpackChunkName: "sub-pages" */ "./Bar.vue");
  5. // 基础页面
  6. const routes = [
  7. { path: "/bar", name: "Bar", component: Bar },
  8. { path: "/foo", name: "Foo", component: Foo },
  9. { path: "/", name: "Home", component: Home },
  10. ];
  11. const router = createRouter({
  12. history: createWebHashHistory(),
  13. routes,
  14. });
  15. export default router;

上面demo中,Home、Foo、Bar 三个组件均动态加载导入,这样当页面切换到相应路由时才会加载对应组件代码。

外置依赖

externals 的主要作用是将部分模块排除在 Webpack 打包系统之外。什么意思呢,就是说,用externals声明了的依赖,webpack会默认已经存在在系统中了,比如在html中用script标签从CDN资源引入,这样就不用webpack再重复打包了。

  1. <script defer crossorigin src="//unpkg.com/react@18/umd/react.development.js"></script>
  1. externals: {
  2. react: "React",
  3. },

虽然结果上看浏览器还是得消耗这部分流量,但结合 CDN 系统特性:

  1. 能够就近获取资源,缩短网络通讯链路;
  2. 能够将资源分发任务前置到节点服务器,减轻原服务器 QPS 负担;
  3. 用户访问不同站点能共享同一份 CDN 资源副本。所以网络性能效果往往会比重复打包好很多。

HTTP 缓存优化

关于http缓存相关的有2点:

  1. 适当的文件拆分策略:比如node_modules里面的内容几乎不变,就可以作为独立的文件来从整体代码中拆出;
  2. 文件命名策略,为文件设置hash占位符;

hash占位符可以设置为:

  • [fullhash]:整个项目的内容 Hash 值,项目中任意模块变化都会产生新的 fullhash;
  • [chunkhash]:产物对应 Chunk 的 Hash,Chunk 中任意模块变化都会产生新的 chunkhash;
  • [contenthash]:内容 Hash 值,仅当产物内容发生变化时才会产生新的 contenthash,实用性较高。
    1. module.exports = {
    2. // ...
    3. entry: { index: "./src/index.js", foo: "./src/foo.js" },
    4. output: {
    5. filename: "[name]-[contenthash:8].js",
    6. path: path.resolve(__dirname, "dist"),
    7. },
    8. plugins: [new MiniCssExtractPlugin({ filename: "[name]-[contenthash].css" })],
    9. };
    当内容没有变化,contenthash的计算值不会变化,故而文件名不变,请求文件时能命中缓存,产物文件不会被重复下载,一直到文件内容发生变化。
    这里有一个问题:异步模块的内容变了,主文件的内容一定变化。这个值得注意,但是很好理解,因为主文件中会依赖异步文件名称。(异步文件内容变化导致异步文件名称变化,进而导致主文件内容变化,导致主文件名称变化)

    Tree Shaking

    tree shaking :在 Rollup 中率先实现,Webpack 自 2.0 版本开始接入,是一种基于 ES Module 规范的 Dead Code Elimination 技术。 它会在运行过程中静态分析模块之间的导入导出,判断哪些模块导出值没有被其它模块使用 —— 相当于模块层面的 Dead Code,并将其删除。

启动 Tree Shaking 功能必须同时满足两个条件:

  1. 配置 optimization.usedExports 为 true(标记模块导入导出列表);
  2. 启动代码优化功能,以下都可以:
  • 配置 mode = production
  • 配置 optimization.minimize = true
  • 提供 optimization.minimizer 数组

    1. // 满足2个条件
    2. // 开启tree shaking
    3. module.exports = {
    4. mode: "production", // 满足条件2
    5. optimization: {
    6. usedExports: true, // 满足条件1
    7. },
    8. };

    Scope Hoisting

    scope hoisting : 将符合条件的多个模块合并到同一个函数空间 中,从而减少产物体积,优化性能。
    提供了三种开启 Scope Hoisting 的方法:

  • 使用 mode = ‘production’ 开启生产模式;

  • 使用 optimization.concatenateModules 配置项;
  • 直接使用 ModuleConcatenationPlugin 插件;

上述三种方法本质都会调用 ModuleConcatenationPlugin 完成模块分析与合并操作。

  1. const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
  2. // 三种方法都能开启scope hoisting
  3. module.exports = {
  4. // 方法1: 将 `mode` 设置为 production,即可开启
  5. mode: "production",
  6. // 方法2: 将 `optimization.concatenateModules` 设置为 true
  7. optimization: {
  8. concatenateModules: true,
  9. usedExports: true,
  10. providedExports: true,
  11. },
  12. // 方法3: 直接使用 `ModuleConcatenationPlugin` 插件
  13. plugins: [new ModuleConcatenationPlugin()]
  14. };

tree shaking 、scope hoisting 底层基于 ES Module 方案的静态特性,推断模块之间的依赖关系,因此注意在非ESM的场景下会失效。

优化 eslint 性能

使用新版本组件 eslint-webpack-plugin 替代旧版 eslint-loader(eslint-webpack-plugin 在模块构建完毕后执行检查,不会阻断文件加载流程,性能更优)
或者其他使用eslint的方式:

  • 编辑器插件完成 ESLint 检查、错误提示、自动 Fix
  • 使用 husky,仅在代码提交前执行 ESLint 代码检查;

    监控产物体积

    Webpack 专门为此提供了一套 性能监控方案,当构建生成的产物体积超过阈值时抛出异常警告,以此帮助我们时刻关注资源体积,避免因项目迭代增长带来过大的网络传输,用法:

  1. module.exports = {
  2. // ...
  3. performance: {
  4. // 设置所有产物体积阈值
  5. // 若此时产物体积超过 170KB,则报错
  6. maxAssetSize: 170 * 1024,
  7. // 设置 entry 产物体积阈值
  8. maxEntrypointSize: 244 * 1024,
  9. // 报错方式,支持 `error` | `warning` | false
  10. hints: "error",
  11. // 过滤需要监控的文件类型
  12. assetFilter: function (assetFilename) {
  13. return assetFilename.endsWith(".js");
  14. },
  15. },
  16. };

最大产物体积的大小多少合适?经验规则是170kb,参考: