AccessGuard 类型缓存治理:tsc --noEmit 3s→45s→8s,编辑器 5s→300ms
现场:编译墙如何形成
AccessGuard v2.4 的权限体系里,权限类型从 20 个膨胀到 100+,策略模板从 5 个增长到 50+,Monorepo 子包从 3 个扩展到 20 个。tsc --noEmit 从最初的 3 秒升到 45 秒,编辑器红色波浪线延迟超过 5 秒,CI 中最慢的环节变成类型检查。类型系统从生产力工具变成瓶颈,这是大项目会撞上的编译墙。
即使 TypeScript 7.0 带来原生级性能,类型复杂度优化仍然是根本性工程实践:无论编译器多快,O(2ⁿ) 的泛型展开终究会触达硬件极限。用编译器友好的方式写类型,是长期生效的能力。
类型计算复杂度与三处缓存
TypeScript 类型计算可以从时间复杂度、空间复杂度、结构复杂度三个维度看。联合类型成员数量为 N 时,分配条件类型计算复杂度至少是 O(N)。Permission 联合类型包含 127 个成员,任何对 Permission 的条件类型操作都可能触发 127 次独立计算。
tsc 的类型缓存有三个核心:Type Cache(类型实例化结果的内存缓存)、Assignability Cache(“A 是否可赋值给 B”的缓存)、结构比较缓存。命中率直接决定编译性能。优化前约 62%,健康水平在 85%+。大量动态生成的映射类型和条件类型无法命中缓存。
缓存本质是去重哈希表:结构等价的类型只计算一次。但有三类模式命中不了:
- Fresh Object:每次字面量对象类型都是“新鲜的”,结构相同也无法命中。
- 条件类型的分配计算:对联合类型做条件类型分配时,需要为每个成员单独构造类型实例。
- 递归类型的深度展开:每次展开到不同深度产生不同中间类型,进一步稀释缓存。
优化方向是减少“不可缓存”的类型操作,提高结构等价类型的复用率。
诊断先行:--generateTrace 看三个指标
不要先改类型。运行 --generateTrace 生成 trace 数据,再用 @typescript/analyze-trace 分析。
tsc --noEmit --generateTrace trace
npx @typescript/analyze-trace trace
关键指标:
- Check time:过高说明类型复杂度(泛型、递归)到瓶颈。
- I/O time:过高说明
paths配置或文件索引量过大,磁盘扫描阻塞。 - Types created:创建类型数量达到数十万级别,类型系统已经过度设计。
治理手段
类型写法瘦身
- 避免过度递归:一个类型需要递归 5 层以上才能推导出结果,考虑用
interface替代type。interface在 TS 内部有更好的缓存机制。 - 限制联合类型规模:巨大的联合类型会强制编译器全量遍历,尽量拆成多个小接口。
- 显式标注返回类型:在复杂函数上标注返回类型,切断推导链,防止编译器在整个调用栈上深层搜索。
- 优化
paths解析:过多通配符*会让模块解析器在文件系统中做大量 I/O 扫描,尽量使用精确路径映射。 - 开启
isolatedModules:强制每个文件可以独立转译,是使用 esbuild 等工具的前提。
tsconfig 常用优化项:target: esnext(避免不必要的 ES5 转换)、module: esnext、skipLibCheck: true(跳过库文件类型检查)、allowJs: false、isolatedModules: true、incremental: true、tsBuildInfoFile;include 精确指定源文件范围,exclude 排除 node_modules 与测试文件。
Project References 与增量编译
Project References 是 TypeScript 3.0 引入的官方 Monorepo 编译方案。核心是把一个大项目拆成多个独立 TypeScript 项目,各自拥有 tsconfig.json,编译时只重新编译发生变化的子项目。大型 Monorepo 必须开启 composite: true。
增量编译用 --incremental 与 .tsbuildinfo 文件缓存上次编译状态,tsBuildInfoFile 指定缓存路径。AccessGuard 最终把编译时间从 45 秒压到 8 秒,编辑器响应从 5 秒降到 300 毫秒,缓存命中率从 62% 提到 85%。
转译与检查解耦
tsc 是运行在 Node.js 上的单线程程序。无论机器有多少核心,类型检查这个最耗时的环节始终在单核上阻塞。当写下 T extends U ? A : B,且 T、U 是数千个成员组成的联合类型时,TS Server 必须在内存中构建庞大的决策树。如果结构递归,计算复杂度会从线性增长演变为指数级增长,直接导致 IDE 的 Intellisense 卡死。
转译是把 TS 转成 JS 的 AST 节点替换过程,不需要关心类型是否正确;类型检查需要扫描整个项目的所有定义。强耦合意味着每次保存文件,都要为毫无变化的类型定义支付计算成本。开发环境停止使用 ts-loader 或 tsc 构建,用 tsx 或 esbuild,把热更新从秒级降到 50ms 以内。类型安全交给 CI/CD 或独立后台进程。在 Webpack 或 Vite 环境中,使用 fork-ts-checker-webpack-plugin,它会启动独立 Node.js 进程执行 tsc --noEmit。
可复用结论
- 编译墙来自联合类型规模、递归泛型、不可缓存类型操作,以及转译与类型检查强耦合。
- 先度量:
--generateTrace+@typescript/analyze-trace,看 Check time、I/O time、Types created。 - 提高缓存命中率:减少 Fresh Object、条件类型分配、递归深度展开;拆小联合类型,复杂递归用
interface,显式返回类型,精确paths。 - Monorepo:Project References +
composite: true+--incremental+.tsbuildinfo/tsBuildInfoFile。 - 开发环境解耦:
tsx/esbuild或fork-ts-checker-webpack-plugin,开启isolatedModules。 - AccessGuard 的量化结果:
tsc --noEmit3s→45s→8s,编辑器 5s→300ms,缓存命中率 62%→85%,HMR 秒级→50ms,联合类型成员 127 个。