ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Farm 持久化缓存(Persistent Cache)完全指南:增量构建原理与配置实战

Farm 持久化缓存(Persistent Cache)完全指南:增量构建原理与配置实战
  • 前端构建
  • 构建工具
  • 开发工具

【免费下载链接】farm

Extremely fast Vite-compatible web build tool written in Rust

项目地址:https://gitcode.com/gh_mirrors/fa/farm
点击查看免费下载

Farm 从v0.14.0起支持将编译结果持久化到磁盘,通过compilation.persistentCache开启后,热启动(hot start)与热构建(hot build)可显著提速,官方文档给出的实测数据显示编译时间最高可减少80%。本文将基于 website/docs/advanced/persistent-cache.md 的完整内容,结合 packages/core/src/config/normalize-config/normalize-persistent-cache.ts 与 crates/core/src/config/persistent_cache.rs 等源码,系统讲解持久化缓存的开关方式、失效校验规则、缓存键生成策略、构建依赖追踪以及插件适配注意事项,帮助你在真实项目中安全、高效地开启并使用这一能力。

一、为什么需要持久化缓存:冷启动与热启动的差距

Farm 本身是基于 Rust 编写的高性能构建工具,单次编译已经很快,但面对大型项目(如 Ant Design Pro 级别的应用),每次启动都重新解析、加载、转换全部模块仍然耗时。持久化缓存的核心思路是:把上一次编译得到的模块转换结果、依赖图信息、资源产物等序列化到磁盘,下次启动时直接复用,跳过可跳过的构建阶段。

官方文档使用 examples/arco-pro 项目给出了冷启动(无缓存)与热启动(有缓存)的对比数据:

阶段冷启动(无缓存)热启动(有缓存)提升幅度
start(启动)1519ms371ms减少 75%
build(构建)3582ms562ms减少 84%

从源码实现看,Farm 的缓存体系由 crates/core/src/cache/mod.rs 中的CacheManager统一管理,内部包含:

  • module_cache:模块及模块依赖图的缓存;
  • resource_cache:最终资源(资源束)的缓存;
  • plugin_cache:各插件自定义数据的缓存;
  • custom / lazy_compile_store:自定义缓存与懒编译缓存。

CacheManager在构造时会根据配置创建CacheType::Disk(磁盘缓存)或CacheType::Memory(内存缓存),磁盘缓存通过DiskCacheFactory落地到指定目录(crates/core/src/cache/mod.rs)。

二、开启与关闭缓存:persistentCache 配置

2.1 一键开启

使用compilation.persistentCache即可开启/关闭缓存:

import { defineConfig } from "@farmfe/core"; export default defineConfig({ compilation: { persistentCache: true, }, });

注意:persistentCache配置在compilation字段下;而buildDependencies示例中出现了直接写在顶层的情况,实际使用时请统一放在compilation.persistentCache内。

2.2 默认值展开:true 等价于完整对象

官方文档明确:persistentCache: true等价于以下完整配置:

({ persistentCache: { // 缓存存储目录 cacheDir: "node_modules/.farm/cache", // 缓存命名空间 namespace: "farm-cache", buildDependencies: [ "farm.config.ts", "@farmfe/core", "@farmfe/plugin-react", // ... 其他所有依赖 ], moduleCacheKeyStrategy: { timestamp: true, hash: true, }, }, });

这一展开逻辑在 Rust 侧有直接印证。在 crates/core/src/config/persistent_cache.rs 中,get_default_config返回的默认对象正是:

  • namespace: "farm-cache"
  • cache_dir: node_modules/.farm/cache(基于 root 解析为绝对路径)
  • module_cache_key_strategy: { timestamp: true, hash: true }
  • envs、build_dependencies为空(后者由 Node 侧填充)

而PersistentCacheConfig::default()返回Bool(true),即不配置时默认开启。配置为false则完全禁用缓存。此外,在 normalize-persistent-cache.ts 中还有一个细节:当检测到禁用缓存的环境开关(isDisableCache())时,persistentCache会被强制置为false。

2.3 完整配置项速查

依据 compilation-options.md,persistentCache的类型定义如下:

export type PersistentCache = | boolean | { namespace?: string; // 默认 'farm-cache' cacheDir?: string; // 默认 'node_modules/.farm/cache' buildDependencies?: string[];// 默认 farm.config 及其全部深层依赖 moduleCacheKeyStrategy?: { timestamp?: boolean; // 默认 true hash?: boolean; // 默认 true }; envs?: Record<string, string>; // 默认 Farm Env(见环境变量与模式) globalBuiltinCacheKeyStrategy?: { define?: boolean; // 默认 true buildDependencies?: boolean; // 默认 true lockfile?: boolean; // 默认 true packageJson?: boolean; // 默认 true env?: boolean; // 默认 true }; };

三、缓存校验(Cache Validation):什么情况会导致缓存失效

缓存并非无条件复用。Farm 在尝试复用缓存前会按以下条件校验,任一条件发生变化,全部缓存立即失效:

  • Env 对象:由persistentCache.envs配置,默认为Farm Env Mode(即process.env.NODE_ENV、process.env.DEV、process.env.PROD),详见 Environment Variables and Modes。
  • lockfile:如果package-lock.json、yarn.lock或pnpm-lock.yaml发生变化,意味着依赖集合有变动,缓存整体失效。
  • 构建依赖(Build Dependencies):由persistentCache.buildDependencies配置,任一构建依赖文件发生变化,全部缓存失效。
  • 缓存命名空间(Cache Namespace):不同namespace下的缓存互不通用。想强制清空全部缓存时,换一个新的namespace即可。
  • 内部缓存版本(Internal Cache Version):Farm 内部维护一个缓存版本号,当 Farm 自身版本变化且可能影响输出(例如渲染优化)时,会提升缓存版本号,触发全部缓存失效。该版本号在 crates/core/src/cache/store/constant.rs 中以常量FARM_CACHE_VERSION(当前为"0.9.1")定义,并作为缓存目录的组成部分。

如果发现缓存不生效,请优先排查以上条件;若缓存已损坏,可直接删除node_modules/.farm/cache目录手动清除。

3.1 缓存目录的磁盘结构

从 crates/core/src/cache/store/disk.rs 可以看到缓存目录的层级组织方式:

{cacheDir}/{FARM_CACHE_VERSION}-{cacheDir 末级目录名}/{namespace}/{development 或 production}/...

即:在cacheDir基础上追加FARM_CACHE_VERSION前缀目录 →namespace子目录 → 按开发/生产模式区分的子目录。这从实现层面印证了「内部缓存版本 + 命名空间 + 环境模式」共同参与缓存隔离的设计,也解释了为何手动清缓存时删掉node_modules/.farm/cache即可。

3.2 自定义环境变量参与缓存键(envs)

使用persistentCache.envs指定额外的环境变量键值对参与缓存键计算。当其中任一值变化时,全部缓存失效:

import { defineConfig } from "@farmfe/core"; export default defineConfig({ compilation: { persistentCache: { envs: { MY_API_URL: process.env.MY_API_URL ?? '', }, }, }, });

在 persistent_cache.rs 中,所有envs键会先排序,再拼接成key=value&key=value...字符串,经sha256哈希后作为一条「配置哈希」追加到buildDependencies中,最终参与全局缓存键计算——因此任何 env 值的变化都会改变哈希,进而使整份缓存失效。

3.3 全局内置缓存键策略(globalBuiltinCacheKeyStrategy)

persistentCache.globalBuiltinCacheKeyStrategy控制哪些内置键参与缓存失效判断。默认情况下,define、buildDependencies、lockfile、packageJson、env这五类变化都会让整份缓存失效,可针对已知不会影响构建的键选择性关闭:

import { defineConfig } from "@farmfe/core"; export default defineConfig({ compilation: { persistentCache: { globalBuiltinCacheKeyStrategy: { define: true, // define 配置变化时失效缓存 buildDependencies: true, // 构建依赖变化时失效缓存 lockfile: true, // lockfile 变化时失效缓存 packageJson: true, // package.json 变化时失效缓存 env: true, // env 模式变化时失效缓存 }, }, }, });

源码中的默认值在 normalize-persistent-cache.ts 中定义,随后与用户配置浅合并,且该策略字段不会透传给 Rust 侧(会在归一化时删除,见第 54 行)。其底层行为是:

  • env: true时,把已解析的用户 env 全部并入persistentCache.envs(第 56-61 行);
  • define: true时,把config.define的每个键值(非字符串值会先JSON.stringify)并入envs(第 63-81 行)——这也解释了为什么源码注释写明「所有 define 选项都应进入 envs」;
  • packageJson: true时,读取 root 下package.json中type、name、exports、browser、main、module六个关键字段,作为package.json[键名]形式的 env 键值(第 86-107 行);
  • lockfile: true时,自动把package-lock.json、yarn.lock、pnpm-lock.yaml追加进buildDependencies(第 113-124 行)。

四、构建依赖(Build Dependencies)

构建依赖是指可能影响编译过程或编译产物的依赖,例如插件或配置文件。任一构建依赖发生变化,全部缓存即失效。

buildDependencies中的每一项可以是文件路径,也可以是包名,例如:

import { defineConfig } from "@farmfe/core"; import path from "node:path"; export default defineConfig({ compilation: { persistentCache: { buildDependencies: [ // 一个文件路径 path.resolve(process.cwd(), "./plugins/my-plugin.js"), // 一个包名,注意该包必须暴露 package.json "farm-plugin-custom-xxx", ], }, }, });

默认情况下,所有配置文件及其依赖都会被纳入;如果希望额外添加一些文件或依赖来触发缓存失效,通过buildDependencies追加即可,一旦这些文件变化,全部缓存失效。

从 normalize-persistent-cache.ts 可以看到默认追踪逻辑的完整实现:

  1. 若存在configFileDependencies直接复用,否则调用traceDependencies深度追踪配置文件的依赖;
  2. 绝对路径的依赖直接加入buildDependencies;
  3. 以字符串/数组形式声明的 Rust 插件(plugins中类型为string或数组的项)会通过require.resolve(${p}/package.json)解析为包名@版本号形式的依赖键(如@farmfe/plugin-react@1.x.x),版本号变化即触发缓存失效。

在 Rust 侧,persistent_cache.rs 会把全部构建依赖排序拼接后统一做sha256哈希,并将该哈希追加到cacheDir后面,作为最终实际使用的缓存目录——构建依赖变了,哈希就变,等于换了一个全新的缓存目录,这就是「全部缓存失效」在实现层面的具体体现。

五、模块缓存键策略(Module Cache Key Strategy)

Farm 提供两种策略控制模块缓存键的生成方式:

  • timestamp:检查模块的更新时间戳,若时间戳未变化,则跳过该模块的构建,性能最优;
  • hash:在load与transform之后检查内容哈希,若内容未变化,则跳过该模块后续的构建阶段。

默认两者同时开启(即{ timestamp: true, hash: true })。两种策略的取舍在于:timestamp最快但依赖文件系统时间戳的准确性;hash更严谨但需要先执行load/transform才能拿到内容做比对。更细化的行为可参考 compilation-options.md:

  • timestamp: true且模块时间戳未变化时,load、transform等构建阶段钩子全部跳过,直接复用缓存的模块;
  • hash: true且内容未变化时,仍会调用load和transform获取转换后的内容,其余钩子跳过并复用缓存模块。

5.1 Node 目标下的自动降级

Farm 会在output.targetEnv为node时自动将timestamp置为false。该逻辑在 normalize-persistent-cache.ts 中实现——因为 Node 环境下模块文件可能由外部进程写入或热更新,时间戳判断不可靠,必须依赖内容哈希保证正确性。

六、插件适配注意事项(Caveats For Plugins)

这是持久化缓存最容易踩坑的地方:当timestamp开启时,load、transform等所有构建阶段钩子都不会被调用。因此,如果插件依赖load/transform收集信息,却未实现plugin_cache_loaded与write_plugin_cache钩子,插件行为可能不符合预期。典型反例:某插件在load与transform中收集信息、在finish钩子中统一输出,就必须实现plugin_cache_loaded(加载缓存)与write_plugin_cache(写入缓存)两个钩子,否则热启动时拿不到数据。

这两个钩子在 Rust 侧是Plugintrait 的一部分(crates/core/src/plugin/mod.rs 与 crates/core/src/plugin/mod.rs):

  • plugin_cache_loaded(&self, cache: &Vec<u8>, context):命中缓存时把插件缓存字节数据交还给插件;
  • write_plugin_cache(&self, context) -> Option<Vec<u8>>:构建结束时插件返回自己的序列化缓存数据。

调用链位于 plugin_driver.rs 与 plugin_driver.rs:plugin_cache_loaded会在构建开始时遍历所有插件,从PluginCacheManager读取以插件名(非字母数字字符替换为_)为键的缓存并逐个回调;write_plugin_cache则在结束时收集各插件返回的数据写入缓存。底层存储由 crates/core/src/cache/plugin_cache.rs 的PluginCacheManager负责,写盘前还会对缓存内容再做一次sha256摘要参与键管理。

另外,Plugintrait 还提供了handle_persistent_cached_module钩子(crates/core/src/plugin/mod.rs):命中持久化缓存的模块会先经过该钩子,返回false即可主动失效该模块的缓存——这是插件在缓存命中时进行二次校验(如自定义校验逻辑)的扩展点。

七、实战排查与调优建议

  1. 确认开启:检查配置文件中compilation.persistentCache是否为true(默认即为 true)或完整对象,并确认没有禁用缓存的环境开关生效。
  2. 看缓存目录:构建后查看node_modules/.farm/cache下是否出现以FARM_CACHE_VERSION前缀命名的目录;目录中包含 namespace 子目录与farm-cache.json清单文件(constant.rs)。
  3. 缓存不生效:按「缓存校验」一节逐条排查——env 是否变化、lockfile 是否变化、buildDependencies 指向的文件是否变化、namespace 是否被改动、Farm 版本是否升级。
  4. 手动清缓存:删除node_modules/.farm/cache目录,或直接换一个namespace。
  5. Node/SSR 场景:output.targetEnv为node时 timestamp 自动关闭,属预期行为,不必手动干预。
  6. 开发自定义插件:只要插件依赖load/transform且需要跨构建保留状态,就必须成对实现plugin_cache_loaded与write_plugin_cache,否则请在缓存策略或插件逻辑上做出取舍。

参考资源

  • 官方文档原文:website/docs/advanced/persistent-cache.md
  • 配置参考:website/docs/config/compilation-options.md(persistentCache小节)
  • 环境变量与模式:website/docs/features/env.md
  • 配置归一化实现:packages/core/src/config/normalize-config/normalize-persistent-cache.ts
  • Rust 侧配置与默认值:crates/core/src/config/persistent_cache.rs
  • 缓存管理器与磁盘存储:crates/core/src/cache/mod.rs、crates/core/src/cache/store/disk.rs
  • 插件缓存机制:crates/core/src/cache/plugin_cache.rs、crates/core/src/plugin/plugin_driver.rs
  • 前端构建
  • 构建工具
  • 开发工具

【免费下载链接】farm

Extremely fast Vite-compatible web build tool written in Rust

项目地址:https://gitcode.com/gh_mirrors/fa/farm
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进