TypeScript 类型检查性能优化:仓库越大为什么越慢,以及怎么拆和怎么测

HTMLPAGE 团队
17 分钟阅读

TypeScript 变慢时,真正该怀疑的通常不是语言本身,而是类型图、项目边界和诊断方式。本文从测量、热点类型、项目拆分和团队治理出发,讲清大型仓库的 typecheck 性能优化方法。

#TypeScript #Typecheck #Performance #Monorepo #Diagnostics

很多团队第一次认真优化 TypeScript 性能,不是因为喜欢研究编译器,而是因为工作流已经被拖慢了:保存后编辑器长时间转圈,CI 里的 typecheck 越来越久,某个看起来不大的 PR 却能让全仓类型检查多出几分钟。这个阶段如果只把问题归结为“TypeScript 太重”,往往会错过真正有效的优化路径。

类型检查变慢通常有三类原因:项目边界没有拆开、类型表达本身过于昂贵、诊断方式过于粗糙。你如果不先分清是哪一类,后面无论是加缓存、调机器配置,还是试图减少 strict 规则,收益都很有限。真正有效的优化,必须从“先知道时间花在哪”开始。

第一步不是优化,而是测量:别在没有诊断的情况下猜

TypeScript 性能问题最常见的误区,是凭感觉调。某个同事说最近条件类型写多了,另一个同事觉得是编辑器缓存坏了,还有人怀疑是某个新包。没有诊断,这些猜测都很难落地。

先做三件事通常最有价值:

  1. tsc --extendedDiagnostics 看总耗时和主要阶段。
  2. tsc --explainFiles 或项目引用关系,确认是不是全仓都被拉进来了。
  3. 在可疑项目上用 --generateTrace 或编辑器 trace,看哪些文件和类型实例化最重。

这些信息的价值不在于“多几个统计数字”,而在于帮团队把问题从“TypeScript 很慢”拆成更具体的结论:是项目图太大,还是某些类型本身成本过高。

先区分两类慢:图太大,还是类型太贵

慢的来源典型表现优先手段
项目图太大改一个包,很多无关项目一起重算项目引用、边界拆分、缓存粒度治理
类型表达太贵单个文件或少量工具类型就能让编辑器明显卡顿简化条件类型、减少分发式展开、拆解递归类型

这两类问题经常同时存在,但优先级不一样。如果全仓所有应用都被拉进同一个检查图,再怎么微调类型技巧,收益都不会太明显;反过来,如果边界已经拆开,但某个工具类型每次都实例化几千次,那项目图再清晰也会被局部热点拖慢。

最容易制造性能热点的,不是“高级类型”这个标签,而是失控的组合

以下几种写法最值得重点怀疑:

  • 对大联合类型做分发式条件展开。
  • 深层递归条件类型没有明确终止边界。
  • 巨大的模板字面量组合类型。
  • 把复杂泛型工具直接暴露给全仓高频类型路径。

比如下面这类模式就很容易在规模变大后变贵:

type Normalize<T> = T extends any
  ? T extends object
    ? { [K in keyof T]: Normalize<T[K]> }
    : T
  : never

单看这段类型定义没有问题,问题在于一旦它被应用在很宽的联合、很深的嵌套对象,实例化次数会迅速膨胀。很多性能问题不是来自某一个“错误类型”,而是多个看似合理的组合在一起后成本失控。

边界拆分经常比微调类型更有效

在大型仓库里,最具性价比的优化往往不是先去改最复杂的工具类型,而是先问:为什么这次检查需要把这么多项目一起算?

更稳的优化顺序通常是:

  1. 用项目引用把独立包拆成可缓存单元。
  2. 确保应用层不会反向依赖共享包内部实现。
  3. 让测试、脚本和生产源码分开检查,而不是全塞一处。
  4. 再回头优化局部高成本类型。

因为只要项目边界还没拆清,任何局部性能优化都可能被更大的依赖图抵消。

一个常见失败案例:为了省导出层,所有类型工具都挂进 shared

某团队把通用条件类型、深只读工具、事件映射、API 响应包装、表单状态工具都集中放到一个共享类型包里。短期看很整齐,长期却造成两个问题:

  • 几乎所有项目都会间接依赖这个包,导致它变成性能热点中心。
  • 一旦这里某个工具类型变复杂,全仓类型检查都被一起拖慢。

问题不在共享本身,而在于“共享层是否承载了过多高成本类型表达”。如果所有复杂推导都集中在一个被全仓引用的地方,性能问题自然会被放大成系统性问题。

团队治理上,最有价值的是把“类型复杂度预算”说清楚

很多性能问题不是一夜之间出现的,而是团队长期默许“能推出来就行”的结果。比较值得固化的规则包括:

  • 高成本工具类型必须配使用边界和示例,不得默认全仓泛用。
  • 复杂条件类型优先局部封装,而不是直接暴露为公共 API。
  • 编辑器明显卡顿的文件,必须在 review 时追踪具体类型来源。
  • 引入新共享类型包时,要评估引用范围和实例化成本,而不是只看复用度。

这类规则的意义不在于限制高级类型,而在于防止高成本类型在没有边界的情况下扩散。

一份类型检查性能排查清单

  • 是否先用诊断命令确认耗时主要集中在哪个阶段。
  • 项目图是否足够细,还是一次改动就拉全仓参与检查。
  • 是否存在高频共享的递归条件类型或超宽联合展开。
  • 测试、脚本、生产源码是否被不必要地绑在同一检查上下文里。
  • 团队是否有针对高复杂度类型的 review 和约束机制。

总结

TypeScript 性能优化真正有效的路径,不是先怀疑语言,而是先识别:到底是项目图过大,还是类型本身过重。边界拆分解决系统性拖慢,局部类型简化解决热点卡顿,诊断工具负责把两者分开。只要团队愿意把“类型复杂度”和“项目边界”一起纳入治理,TypeScript 在大型仓库里完全可以既严格又可用。

本批次专题导航:

本系列导航:

延伸阅读: