Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack持久化缓存 #1

Open
JayFate opened this issue May 11, 2023 · 1 comment
Open

webpack持久化缓存 #1

JayFate opened this issue May 11, 2023 · 1 comment

Comments

@JayFate
Copy link
Owner

JayFate commented May 11, 2023

出于构建安全考虑,默认情况下 webpack 不会启用持久化缓存。

一个典型的持久化缓存配置:

cache: {
    type: "filesystem",
    buildDependencies: {
        config: [ __filename ] // 当你 CLI 自动添加它时,你可以忽略它
    }
}

为了处理构建过程中的依赖关系,webpack 提供了三个新工具:

构建依赖(Build dependencies)

此为全新的配置项 cache.buildDependencies,它可以指定构建过程中的代码依赖。为了使它更简易,webpack 负责解析并遵循配置值的依赖。

值类型有两种:文件和目录。目录类型必须以斜杠(/)结尾。其他所有内容都解析为文件类型。

对于目录类型来说,会解析其最近的 package.json 中的 dependencies。对于文件类型来说,我们将查看 node.js 模块缓存以寻找其依赖。

示例:构建通常取决于 webpack 本身的 lib 文件夹:你可以这样配置:

cache.buildDependencies: {
    defaultWebpack: ["webpack/lib/"]
}

webpack/lib 或 webpack 依赖的库(如,watchpackenhanced-resolved 等)发生任何变化时,其缓存将失效。webpack/lib 已是默认值,默认情况下无需配置。

另一个示例:构建依旧取决于你的配置文件。具体配置如下:

cache.buildDependencies: {
    config: [__filename]
}

__filename 变量指向 node.js 中的当前文件。

当配置文件或配置文件中通过 require 依赖的任何内容发生更改时,也会使得持久化缓存失效。当配置文件通过 require() 引用了所有使用过的插件时,它们也会成为构建依赖项。

如果配置文件通过 fs.readFile 读取文件,则将不会成为构建依赖项,因为 webpack 仅遵循 require()。你需要手动将此类文件添加到 buildDependencies 中。

缓存版本(Version)

构建的某些依赖项不能单纯的依靠对文件的引用,如,从数据库读取的值,环境变量或命令行上传递的值。对于这些值,我们给出了新的配置项 cache.version, 类型为 string。

cache: {
    version: `${process.env.GIT_REV}`
}

缓存名(Name)

在某些情况下,依赖关系会在多个不同的值间切换,并且对于每个值更改都会使得持久化缓存失效,这显然是浪费资源的。对于这类值,我们给出了新的配置项 cache.name

cache.name 类型为 string。传递值将创建一个隔离且独立的持久化缓存。

cache.name 被用于对文件名进行持久化缓存。确保仅传递短小且 fs-safe 的名称。

示例:你的配置可以使用 --env.target mobile|desktop 参数为移动端或 PC 用户创建不同的构建。具体配置如下:

cache: {
    name: `${env.target}`
}

managedPaths

webpack 默认会忽略对 node_modules 的缓存分析,以避免过大的不必要的性能损耗。可以通过配置 cache.managedPaths: [] 禁用此行为,或者加入其他需要忽略的目录。

Watching

watch 状态并设置 cache.type: "filesystem" 时,webpack 会在内部以分层方式启用文件系统缓存和内存缓存。从缓存读取时,会先查看内存缓存,如果内存缓存未找到,则降级到文件系统缓存。写入缓存将同时写入内存缓存和文件系统缓存。

文件系统缓存会等到编译过程完成且编译器处于空闲状态才进行缓存,以避免额外延迟编译过程。

cache.idleTimeoutcache.idleTimeoutForInitialStore,它们控制着持久化缓存之前编译器必须空闲的时长。cache.idleTimeout 默认为 60s,cache.idleTimeoutForInitialStore 默认为 0s。由于序列化阻止了事件循环,因此在序列化缓存时不进行缓存检测。此延迟尝试避免由于快速编辑文件,而在 watch 模式下导致重新编译造成的延迟,同时尝试为下一次冷启动保持持久化缓存的最新状态。这是一个折中的解决方案,可以设置适合你工作流的值。较小的值会缩短冷启动时间,但会增加延迟重新构建的风险。

错误处理

发生错误要恢复持久化缓存的方式,可以通过删除整个缓存并进行全新的构建,或者通过删除有问题的缓存 entry 并使得该项目保持未缓存状态来进行。

# 删除缓存
rm -rf ./node_modules/.cache/webpack

也可以开启 infrastructureLogging 来进行 debug

module.exports = {
  infrastructureLogging: {
    debug: /webpack\.cache/
  },
};

Webpack 缓存的内部工作流

  • webpack 读取缓存文件。

    • 没有缓存文件 -> 未构建缓存
    • 缓存文件中的 versioncache.version 不匹配 -> 没有构建缓存
  • webpack 将解析快照(resolve snapshot)与文件系统进行对比

    • 匹配到 -> 继续后续流程

    • 没有匹配到:

      • 再次解析所有解析结果(resolve results

        • 没有匹配到 -> 未构建缓存
        • 匹配到 -> 继续后续流程
  • webpack 将构建依赖快照(build dependencies snapshot)与文件系统进行对比

    • 没有匹配到 -> 未构建缓存
    • 匹配到 -> 继续后续流程
  • 对缓存 entry 进行反序列化(在构建过程中对较大的缓存 entry 进行延迟反序列化)

  • 构建运行(有缓存或没有缓存)

    • 追踪构建依赖关系

      • 追踪 cache.buildDependencies
      • 追踪已使用的 loader
  • 新的构建依赖关系已解析完成

    • 解析依赖关系已追踪
    • 解析结果已追踪
  • 创建来自所有新解析依赖项的快照

  • 创建来自所有新构建依赖项的快照

  • 持久化缓存文件序列化到磁盘

序列化

所有支持序列化的 class 都需要注册一个序列化器,基本数据类型和引用数据类型(string,number,Array,Set,Map,RegExp,plain objects,Error)的序列化器都已被注册,如果自定义的模块(module)需要序列化,就需要对改模块注册序列化器:

webpack.util.serialization.register(Constructor, request, name, serializer);

Constructor 应为一个模块(module) class 或构造器函数,配合模块序列化器,用于模块序列化。

request 相当于注册的模块序列化器的 id ,webpack 内部通过 require(request) 加载对应的模块序列化器。

name 被用于区分具有相同 request 的多个模块序列化器调用。

serializer 是至少拥有 serializedeserialize 两个方法的对象。

当需序列化对象时,请调用 serializer.serialize(object, context)context 是至少拥有一个 write(anything) 方法的对象 此方法将内容写入输出流。传递的值也会被序列化。

当需要反序列化对象时,请调用 serializer.deserialize(context)context 是至少拥有一个 read(): anything 方法的对象。此方法会反序列化输入流中的某些内容。deserialize 必须返回反序列化后的对象。

serializedeserialize 应以相同的顺序读取和写入相同的对象。

示例:

// some-module/lib/MyClass.js
class MyClass {
    constructor(a, b) {
        this.a = a;
        this.b = b;
        this.c = undefined;
    }
}

register(MyClass, "some-module/lib/MyClass", null, {
    seralize(obj, { write }) {
        write(obj.a);
        write(obj.b);
        write(obj.c);
    }
    deserialize({ read }) {
        const obj = new MyClass(read(), read());
        obj.c = read();
        return obj;
    }
});
@JayFate
Copy link
Owner Author

JayFate commented May 11, 2023

缓存节省了哪些步骤的执行时间

如果某个模块命中缓存,会节省 compiler.hooks.compilation 这个 hooks 之前的所有步骤,比如解析模块(resolve)、编译模块(loader处理模块)的时间,compiler.hooks.compilation 之后的 hooks 依次正常触发。

因此,可以通过将部分 compiler.hooks.compilation 之后执行的工作提前到这个 hooks 之前执行,比如放在 loader 中执行,这样就能利用缓存获得更大的性能提升

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant