Skip to content

前端打包构建优化

现在前端框架项目,都是经过构建工具(Webpack、Vite、Rollup 等)打包生成。这一步也是 优化网站访问速度的主要手段

如果你是使用 Webpack 打包工具,优化和分析构建产物可以使用 webpack-bundle-analyzer 插件。

下面是我经历过后的一些经验和建议,一起来看下~

1、按需加载路由

在一个大型应用中,通常会设定多个路由页面。默认这些页面会被 Webpack 打包到同一个 chunk 文件中(main.js)。

在这种情况下,首屏渲染时就会出现白屏时间过长问题。

因此,我们可以将每个页面路由组件,拆成单独的一个个 chunk 文件,这样 main.js 文件体积降低,在首屏加载时,不会再加载其他页面的资源,从而提升首屏渲染速度。

要将路由组件拆分成 chunk 也很简单,使用异步 API import() 函数导入组件,Webpack 在打包时会将异步导入拆分成单独的 chunk 文件。

js
const Home = LazyComponent("Home", () => import(/* webpackChunkName: "Home" */ "@/pages/Home"));
... 其他路由组件导入

<Route exact path="/" component={Home} />
... 其他路由组件

LazyComponent 是自行封装的一个懒加载组件,在拿到 import() 异步加载组件内容后,渲染对应组件。

2、合理进行分包

在 Webpack5 中有一个配置选项 splitChunks,可以用来拆包和提取公共代码。

  1. 拆包:将一些模块拆分到单独的 chunk 文件中,如将第三方模块拆分到 vendor.js 中;
  2. 提取公共代码:将多处引入的模块代码,提取到单独的 chunk 文件中,防止代码被重复打包,如拆分到 common.js 中。

如下是一个分包配置的示例:

js

// 拆分 chunks
splitChunks: {
  // cacheGroups 配置拆分(提取)模块的方案(里面每一项代表一个拆分模块的方案)
  cacheGroups: {
    // 禁用默认的缓存组,使用下方自定义配置
    defaultVendors: false,
    default: false,
    // 将 node_modules 中第三方模块抽离到 vendors.js 中
    vendors: {
      // chunk 名称
      name: 'chunk-vendors',
      test: /[\\/]node_modules[\\/]/,
      priority: -10,
      chunks: 'all',
    },
    // 按照模块的被引用次数,将公共模块抽离到 common.js 中
    common: {
      // chunk 名称
      name: 'chunk-common',
      priority: -20,
      chunks: "all",
      // 模块被引入两次及以上,拆分到 common chunk 中
      minChunks: 2,
    },
    // 将 React 生态体系作为一个单独的 chunk,减轻 chunk-vendors 的体积
    react: {
      test: /[\\/]node_modules[\\/](react|react-dom|redux|react-redux|history|react-router|react-router-dom)/,
      name: 'chunk-react-package',
      priority: 0,
      chunks: 'all',
      enforce: true,
    },
  }
},

3、启用代码压缩

使用 TerserWebpackPluginCssMinimizerPlugin 插件对 JS/CSS 代码进行压缩,降低打包资源体积。

js
const TerserWebpackPlugin = require("terser-webpack-plugin"); // JS 压缩
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); // CSS 压缩

...
optimization: {
  minimize: isEnvProduction,
  // 配置压缩工具
  minimizer: [
    new TerserWebpackPlugin({
      // 不提取注释到单独的 .txt 文件
      extractComments: false,
      // 使用多进程并发运行以提高构建速度。并发运行的默认数量:os.cpus().length - 1。
      parallel: true,
    }),
    new CssMinimizerPlugin(), // CSS 压缩
  ],
  ...
}

4、配置特定模块查找路径

如果你的项目或老项目中有使用过 moment.js 时,默认 Webpack 打包会包含所有语言包,导致打包文件非常大。通过配置 ContextReplacementPlugin,可以仅包含特定的语言包,如 zh-cn 和 en,从而减小打包文件的大小 ‌。

js
plugins: [
  new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn|zh-hk|en/),
];

5、externals 指定外部模块

比如 JQuery,一般我们为了减少 vendors chunk 文件的体积,会采用 CDN script 方式引入。

为了在模块中继续使用 import 方式引入 JQuery(import $ from 'jquery'),同时期望 Webpack 不要对路径为 jquery 的模块进行打包,可以配置 externals 外部模块选项。

js
module.exports = {
  //...
  externals: {
    // jquery 通过 script 引入之后,全局中即有了 jQuery 变量
    jquery: "jQuery",
  },
};

6、静态图片管理

在你的项目中可能有这样一个 loader 配置:使用 url-loader 将小于某一阈值的图片,打包成 base64 形式到 chunk 文件中。

js
{
  test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.webp$/],
  loader: require.resolve('url-loader'),
  options: {
    limit: '10000', // 大于10k,打包单独静态资源,否则处理成 base64
    name: 'static/media/[name].[contenthash:8].[ext]', // 图片资源添加版本号
  },
},

看着好像没啥问题,不过现在有一场景:有一组(20 个)文件封面图,平均文件体积大小在 9kb 左右,由于不满足阈值,最终会被打包成 base64 形式放到 chunk 中。

这会导致 chunk 文件中多了将近 200kb 的大小。建议这类一组文件图的情况,可以选择存放到 CDN 静态资源服务器上进行使用。

7、明确目标环境

我们编写的 ES6+ 代码会经过 Babel 进行降级转换和 polyfill 后得到 ES5 代码运行在浏览器上,不过这会增大 chunk 资源的体积。

但是如果明确我们的网站仅运行在主流浏览器上,不考虑 IE11,可以将构建目标调整为 ES2015,这样可以减少 Babel 的降级处理和 polyfill 的引入,减小打包后的 chunk 资源体积

以下是一个 Babel 配置的示例:

js
{
  test: /\.(js|mjs|jsx|ts|tsx)$/,
  include: [path.resolve(context, 'src')],
  loader: require.resolve('babel-loader'),
  options: {
    presets: [
      ['@babel/preset-env', {
        "targets": {
          // - 配置表示支持市场份额大于 0.2% 的浏览器,不包括已停止维护的浏览器和 Opera Mini。(此配置打包后是 es6+ 代码)
          "browsers": ["> 0.2%", "not dead", "not op_mini all"],
          // - 如果期望打包后是 es5 代码,可以使用下面配置,确保 Babel 会将代码转换为 ES5 语法,以兼容 IE 11 及更旧的浏览器。
          // "browsers": ["> 0.2%", "ie 11"]
        },
        // Babel 会根据代码中使用的特性自动引入必要的 polyfill(通过 core-js),以确保这些特性在目标环境中可用。
        "useBuiltIns": "usage",
        "corejs": 3
      }],
      '@babel/preset-react',
      '@babel/preset-typescript',
    ],
    ...
  }
},

8、优化构建速度

打包构建速度过慢,也会影响我们的工作效率。可以从以下几个方向入手:

1、配置模块查找范围

通过 resolve 选项配置模块的查找范围和文件扩展名。

js
// 模块解析
resolve: {
  // 解析模块时应该搜索的目录
  modules: ['node_modules'],
  extensions: ['.ts', '.tsx', '.js', '.jsx'], // 查找模块时,为不带扩展名的模块路径,指定要查找的文件扩展名
  ...
},

2、配置 babel-loader 编译范围

通过 exclude、include 配置来确保编译尽可能少的文件。

js
const path = require("path");
module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.js[x]?$/,
        use: ["babel-loader"],
        include: [path.resolve(__dirname, "src")],
      },
    ],
  },
};

3、开启 babel-loader 编译缓存

设置 cacheDirectory 属性开启编译缓存,避免 Webpack 在每次构建时产生高性能消耗的 Babel 编译过程。

js
{
  test: /\.(js|mjs|jsx|ts|tsx)$/,
  include: [path.resolve(context, 'src')],
  loader: require.resolve('babel-loader'),
  options: {
    ...
    // 开启编译缓存,缓存 loader 的执行结果(默认缓存目录:node_modules/.cache/babel-loader),提升构建速度(作用和单独使用 cache-loader 一致)
    cacheDirectory: true,
    // 配合 cacheDirectory 使用,设置 false 禁用缓存文件压缩,这会增加缓存文件的大小,但会减少压缩的消耗时间,提升构建速度。
    cacheCompression: false,
  }
},

4. 启用多进程对 JS 代码压缩

在使用 TerserWebpackPlugin 对 JS 代码进行压缩时,默认选项 parallel = true 就开启了多进程并发运行,以提高构建速度。

js
new TerserWebpackPlugin({
  // 使用多进程并发运行以提高构建速度。并发运行的默认数量:os.cpus().length - 1。
  parallel: true,
  ...
}),

Released under the MIT License.