24. 提升构建性能

286 阅读4分钟

一、通用环境

1. 更新到最新版本

  • 使用最新的 Webpack 版本,Webpack 团队会经常进行性能优化;
  • 将 Node.js 更新到最新版本,也有助于提高性能;
  • 将 package 管理工具(如 npm 或者 yarn )更新到最新版本,也有助于提高性能;

较新的版本能够建立更高效的模块树以及提高解析速度。

2. loader

将 loader 应用于必要模块,而不是全部模块。使用 include,将 loader 应用在实际需要转换的模块。

// webpack.config.js

const path = require('path');
module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                include: path.resolve(__dirname, 'src'),  // 只解析 src 目录中的 .js 文件
                loader: 'babel-loader',
            },
        ],
    },
}

3. 尽量少的使用工具

每个额外的 loader / plugin 都有其启动时间,工具用的越多所需要的打包时间就越长。

4. 解析

以下步骤可以提高解析速度:

  • 减少 resolve.modulesresolve.extensionsresolve.mainFilesresolve.descriptionFiles 中的条目数量,因为它们会增加文件系统调用的次数;
  • 如果你不使用 syslinks(例如 npm link 或者 yarn link),可以设置 resolve.symlinks: false
  • 如果你使用自定义 resolve plugin 规则,并且没有指定 context 上下文,可以设置 resolve.cacheWithContext: false

5. 小即是快

减小编译结果的整体大小,可以提高构建性能(尽量保持 chunk 体积小)。

  • 使用数量更少/体积更小的 library;
  • 在多页面应用程序中使用 SplitChunksPlugin,并开启 async 模式;
  • 移除未引用代码;
  • 只编译当前正在开发的那些代码。

6. 持久化缓存

  • 在 Webpack 配置中使用 cache 选项;
  • 使用 package.json 中的 postinstall 清除缓存目录。

cache 类型设置为内存或者文件系统:

// webpack.config.js

module.exports = {
    cache: {
        type: 'memory',  // memory 选项告诉 Webpack 在内存中存储缓存,不允许额外的配置
    },
}

7. 自定义 plugin/loader

自定义的 loader 和 plugin,要对它们进行概要分析,以免引入性能问题。

8. dll

使用 DllPlugin 为更改不频繁的代码生成单独的编译结果。这可以提高应用程序的编译速度,但是它增加了构建过程的复杂度。

dll 示例代码

从下图可以看出,使用了 dll 后,打包时间和 bundle 体积都有了大幅度缩减。

image.png

image.png

9. worker 池(worker pool)

thread-loader 可以将非常消耗资源的 loader 分流给一个 worker pool。

对于小的应用或打包速度已经很快的应用,使用 worker 反而会增加打包时间,因为 Node.js 的 runtime 和 loader 都有一定的启动开销,所以是否使用 worker 应该根据情况而定,建议不要使用太多的 worker。

使用方法,就是把 thread-loader 定义在一个 loader 前面,这样就会把这个 loader 放到 worker pool 里去运行(其实就是放到了另外一个 CPU 里去运行),以此来提高打包速度。

worker 示例代码

关键代码:

// webpack.config.js

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            presets: ['@babel/preset-env']
                        }
                    },
                    {   // 注意 thread-loader 应该放在后面,它会先运行,开启一个 worker pool,
                        // 然后把后运行的 babel-loader 放到 worker pool 中执行
                        loader: 'thread-loader',
                        options: {
                            workers: 2  // worker pool(CPU)的数量
                        }
                    }
                ]
            }
        ]
    }
}

二、开发环境

1. 增量编译

使用 Webpack 的 watch mode(监听模式),不使用其他工具来 watch 文件和调用 Webpack。内置的 watch mode 会记录时间戳并将此信息传递给 compilation 以使缓存失效。

在某些配置环境中,watch mode 会回退到 poll mode(轮询模式),监听多个文件会导致 CPU 大量负载。在这些情况下,可以使用 watchOptions.poll 来增加轮询的间隔时间。

2. 在内存中编译

下面几个工具通过在内存中(而不是写入磁盘)编译和 serve 资源来提高性能:

  • webpack-dev-server
  • webpack-hot-middleware
  • webpack-dev-middleware

3. stats.toJson 加速

Webpack4 默认使用 stats.toJson() 输出大量数据。除非在增量步骤中做必要的统计,否则请避免获取 stats 对象的部分内容。

webpack-dev-server 在 v3.1.3 以后的版本,包含一个重要的性能修复,即最小化每个增量构建步骤中,从 stats 对象获取的数据量。

4. Devtool

需要注意的是不同的 devtool 设置,会导致性能差异:

  • eval 具有最好的性能,但并不能帮助你转译代码;
  • 如果你能接受稍差一些的 map 质量,可以使用 cheap-source-map 变体配置来提高性能;
  • eval-source-map 变体配置进行增量编译;
  • 在大多数情况下,最佳选择是 eval-cheap-module-source-map

5. 避免在生产环境下才会用到的工具

某些 utility、plugin 和 loader 都只用于生产环境。例如,在开发环境下使用 TerserPlugin 来压缩和混淆破坏代码是没有意义的。通常在开发环境下,应该排除以下这些工具:

  • TerserPlugin
  • [fullhash] / [chunkhash] / [contenthash]
  • AggressiveSplittingPlugin
  • AggressiveMergingPlugin
  • ModuleConcatenationPlugin

6. 最小化 entry chunk

Webpack 只会在文件系统中输出已经更新的 chunk。对于某些配置选项(如 HMRoutput.chunkFilename[name][chunkhash][contenthash][fullhash])来说,除了对已经更新的 chunk 无效之外,对于 entry chunk 也不会生效。

确保在生成 entry chunk 时,尽量减少其体积以提高性能。

下面的配置为运行时代码创建了一个额外的 chunk,所以它的生成代价较低:

module.exports = {
    optimization: {
        runtimeChunk: true,
    },
}

7. 避免额外的优化步骤

Webpack 通过执行额外的算法任务,来优化输出结果的体积和加载性能。这些优化适用于小型代码库,但是在大型代码库中却非常耗费性能。

module.exports = {
    optimization: {
        removeAvailableModules: false,
        removeEmptyChunks: false,
        splitChunks: false,
    },
}

8. 输出结果不携带路径信息

Webpack 会在输出的 bundle 中生成路径信息。然而,在打包数千个模块的项目中,这会导致造成垃圾回收性能压力。在 options.output.pathinfo 设置中关闭:

module.exports = {
    output: {
        pathinfo: false,
    },
}

9. Node.js 版本 8.9.10-9.11.1

Node.js v8.9.10 - v9.11.1 中的 ES2015 Map 和 Set 实现,存在性能回退问题。Webpack 大量地使用这些数据结构,因此这次回退也会影响编译时间。之前和之后的 Node.js 版本不受影响。

10. TypeScript loader

你可以为 loader 传入 transpileOnly 选项,以缩短使用 ts-loader 时的构建时间。使用此选项,会关闭类型检查。如果要再次开启类型检查,请使用 ForkTsCheckerWebpackPlugin。使用此插件会将检查过程移至单独的进程,可以加快 TypeScript 的类型检查和 ESLint 插入的速度。

module.exports = {
    test: /\.tsx?$/,
    use: [
        {
            loader: 'ts-loader',
            options: {
                transpileOnly: true,
            },
        },
    ],
}

三、生产环境

source map 相当消耗资源,生产环境中尽量不要使用它。