写点什么

Webpack 性能之使用 Cache 提升构建性能

作者:编程江湖
  • 2021 年 11 月 26 日
  • 本文字数:3651 字

    阅读完需:约 12 分钟

不知不觉,Webpack 原理系列已经陆续出了十篇文章,以构建主流程为纲逐步递进到插件、Loader、模块、运行时、Chunk、依赖对象、模块依赖图等关键概念的含义与运行原理,再到 HMR、Tree-Shaking 等特性的功能介绍和原理解析,满满当当十篇文章,合计超过 5W 字,基本上已经贯彻 Webpack 整个核心流程。

接下来我会继续沿着 Webpack 这个少人问津的方向,推出两个实用性更强的系列:基础应用、性能优化。性能优化系列主要介绍在 Webpack 场景下如何通过配置、插件等手段,优化构建与运行性能,以及这些性能优化背后的核心原理,例如本文即将介绍的 Webpack5 全新的 cache 功能。

使用持久化缓存

经过这么多年发展,Webpack 生态在前端工程化能力方面已经发展的非常全面且强大,但大而全的背后其运行性能却逐渐为行业诟病,后进如 Vite、SnowPack 等以性能著称的同类框架更是在业内掀起不小波澜。为此,Webpack 终于在第 5 个大版本引入持久化缓存,提升运行性能。

持久化缓存算得上是 Webpack 5 最令人振奋的特性之一,它能够将首次构建结果持久化到本地文件系统,在下次执行构建时跳过一系列解析、链接、编译等非常消耗性能的操作,直接复用 module、chunk 的构建结果。

使用持久化缓存后,构建性能有巨大提升!以 Three.js 为例,该项目包含 362 份 JS 文件,合计约 3w 行代码,算得上中大型项目:



配置 babel-loader、eslint-loader 后,在我机器上测试,未使用 cache 特性时构建耗时大约在 11000ms 到 18000ms 之间;启动 cache 功能后第二次构建耗时降低到 500ms 到 800ms 之间,两者相差接近 「50」 倍!

而这接近 50 倍的性能提升,仅仅需要在 Webpack5 场景下设置 cache.type = 'filesystem' 即可开启:

module.exports = { 

    //... 

    cache: { 

        type: 'filesystem' 

    }, 

    //... 

}; 

原理

那么,为什么开启持久化缓存之后构建性能会有如此巨大的提升呢?一言蔽之,Webpack5 会将首次构建出的 Module、Chunk、ModuleGraph 等对象序列化后保存到硬盘中,后面再运行的时候就可以跳过一些耗时的编译动作,直接复用缓存信息。

构建流程

在《Webpack 原理系列》中,我们已经深入聊了很多关于 Webpack 构建功能的运行流程与实现细节的内容,为了加深对缓存的理解,这里有必要从构建性能角度简单回顾一下。

Webpack 的构建过程大致上可划分为三个阶段:

初始化,主要是根据配置信息设置内置的各类插件

Make - 构建阶段,从 entry 模块开始,执行:

  • 读入文件内容

  • 调用 Loader 转译文件内容

  • 调用 acorn 生成 AST 结构

  • 分析 AST,确定模块依赖列表

  • 遍历模块依赖列表,对每一个依赖模块重新执行上述流程,直到生成完整的模块依赖图 —— ModuleGraph 对象

Seal - 生成阶段,过程:

  • 代码转译,如 import 转换为 require 调用

  • 分析运行时依赖

  • 遍历模块依赖图,对每一个模块执行:

  • 合并模块代码与运行时代码,生成 chunk

  • 执行产物优化操作,如 Tree-shaking

  • 将最终结果写出到产物文件



过程中存在许多 CPU 密集型操作,例如调用 Loader 链加载文件时,遇到 babel-loader、eslint-loader、ts-loader 等工具时可能需要重复生成 AST;分析模块依赖信息时则需要遍历 AST,执行大量运算;Seal 阶段也同样存在大量 AST 遍历,以及代码转换、优化操作,等等。

实现缓存

在引入持久化缓存之前,Webpack 在每次运行时都需要对所有模块完整执行上述构建流程,假设业务项目中有 1000 个文件,则每次执行 npx webpack 命令时都需要从 0 开始执行 1000 次构建、生成逻辑。

而 Webpack5 的持久化缓存功能则尝试将构建结果保存到文件系统中,在下次编译时对比每一个文件的内容哈希或时间戳,未发生变化的文件跳过编译操作,直接使用缓存副本,减少重复计算;发生变更的模块则重新执行编译流程。缓存执行时机如下图:



如图,Webpack 在首次构建完毕后将 Module、Chunk、ModuleGraph 三类对象的状态序列化并记录到缓存文件中;在下次构建开始时,尝试读入并恢复这些对象的状态,从而跳过执行 Loader 链、解析 AST、解析依赖等耗时操作,提升编译性能。

用法详解

理解缓存的核心原理后,我们再回过头来看看 cache 提供的配置项列表,下面摘录几个比较常用的配置项:

官方文档:https://webpack.js.org/configuration/cache

  • cache.type:缓存类型,支持 'memory' | 'filesystem',需要设置 filesystem 才能开启持久缓存

  • cache.cacheDirectory:缓存文件存放的路径,默认为 node_modules/.cache/webpack

  • cache.buildDependencies:额外的依赖文件,当这些文件内容发生变化时,缓存会完全失效而执行完整的编译构建,通常可设置为项目配置文件,如:

module.exports = { 

  cache: { 

    buildDependencies: { 

      config: [path.join(__dirname, 'webpack.dll_config.js')], 

    }, 

  }, 

}; 

  • cache.managedPaths:受控目录,Webpack 构建时会跳过新旧代码哈希值与时间戳的对比,直接使用缓存副本,默认值为 ['./node_modules']

  • cache.profile:是否输出缓存处理过程的详细日志,默认为 false

  • cache.maxAge:缓存失效时间,默认值为 5184000000

使用时通常关注上述配置项即可,其它如 idleTimeout、idleTimeoutAfterLargeChanges 等项均与 Webpack 内部实现算法有关,与缓存效果关系不大,无需关注。

Webpack 4 中的缓存

实际上,Webpack 4 已经内置使用内存实现的临时缓存功能,但必须在 watch 模式下使用,进程退出后立即失效,实用性不高。不过,在 Webpack 4 及之前版本中可以使用一些 loader 自带的缓存功能提升构建性能,例如 babel-loader、eslint-loader、cache-loader 。

开启 babel-loader 缓存

只需设置 cacheDirectory = true 即可开启 babel-loader 持久化缓存功能,例如:


module.exports = { 

    // ... 

    module: { 

        rules: [{ 

            test: /\.m?js$/, 

            loader: 'babel-loader', 

            options: { 

                cacheDirectory: true, 

            }, 

        }] 

    }, 

    // ... 

}; 

配置项说明:https://github.com/babel/babel-loader#options

以 Three.js 为例,开启缓存后生产环境构建耗时从 3500ms 降低到 1600ms;开发环境构建从 6400ms 降低到 4500ms,性能提升约 30% ~ 50% 。

默认情况下,babel-loader 会将缓存内容保存到 node_modules/.cache/babel-loader 目录,用户也可以通过 cacheDirectory = 'dir' 方式设置缓存路径。

开启 eslint-loader 缓存

eslint-loader 同样支持缓存功能,只需设置 cache = true 即可开启,如:

module.exports = { 

    // ... 

    module: { 

        rules: [{ 

            test: /\.js$/, 

            exclude: /node_modules/, 

            loader: 'eslint-loader', 

            options: { 

                cache: true, 

            }, 

        }, ] 

    }, 

    // ... 

}; 

配置项说明:https://github.com/webpack-contrib/eslint-loader#cache

依然以 Three.js 为例,开启缓存后生产环境构建耗时从 6400ms 降低到 1400ms;开发环境构建从 7000ms 降低到 2100ms,性能提升达到 70% ~ 80%。

默认情况下,babel-loader 会将缓存内容保存到 ./node_modules/.cache/eslint-loader 目录,用户也可以通过 cache = 'dir' 方式设置缓存路径。

使用 cache-loader

除 babel-loader、eslint-loader 这类特化 loader 自身携带的缓存功能外,Webpack 4 中还可以使用 cache-loader 实现与 Webpack 5 相似的通用持久化缓存功能,使用上只需将 cache-loader 配置在 loader 数组首位,例如:


const path = require("path"); 

const webpack = require("webpack"); 


module.exports = { 

    // ... 

    module: { 

        rules: [{ 

            test: /\.js$/, 

            use: ['cache-loader', 'babel-loader', 'eslint-loader'] 

        }] 

    }, 

    // ... 

}; 

cache-loader 文档:https://www.npmjs.com/package/cache-loader

使用 cache-loader 后,生产环境构建耗时从 10602ms 降低到 1540ms;开发环境构建从 11130ms 降低到 4247ms,性能提升约 「60% ~ 80%」。

与 Webpack 5 自带的持久化缓存不同,cache-loader 仅 Loader 执行结果有效,缓存范围与深度不如内置的缓存功能,所以性能收益相对较低,但在 Webpack 4 版本下已经不失为一种简单而有效的性能优化手段。前端培训

总结

网络上关于 Webpack 持久化缓存的讨论特别少,但这确实是 Webpack 5 引入的一个特别让人振奋的功能,甚至在某些情况下能够让构建性能达到 Unbundle 方案的量级,相信随着 Webpack 5 的推广覆盖,持久化缓存会成为 Webpack 性能优化的一大利器。

用户头像

编程江湖

关注

IT技术分享 2021.11.23 加入

还未添加个人简介

评论

发布
暂无评论
Webpack 性能之使用 Cache 提升构建性能