这节在地图里是"编译原理"的落地段:五阶段里最慢的是类型检查(6.1),这一节专门治它,并讲怎么读懂 checker 抛出的报错。大型项目"保存等五秒、检查卡半分钟"是常见的契约代价,调得动才推得动。
别凭感觉优化。用 --extendedDiagnostics 看各阶段耗时。
npx tsc --extendedDiagnostics # 输出含:Files、Lines、Nodes、Identifiers、Symbols、Types、Instantiations、 # Check time、compile time 等分项
输入输出:终端打印各部分耗时。若 Check time 占大头,说明是类型计算重;若 I/O 或 Program 建立慢,可能是文件太多或 include 范围过大。
// 反例:一个文件里塞了上千个内联类型,checker 每次都要整体分析 // 正例:拆模块,让 checker 能利用增量缓存逐文件处理
类型实例化(Instantiations)数量过多是典型瓶颈。复杂泛型被反复实例化,会指数级拖慢。
npx tsc --extendedDiagnostics 2>&1 | grep Instantiations # 若 Instantiations 极高(数十万),说明泛型过度展开
import 找不到或慢,用 traceResolution 看编译器怎么找文件。
npx tsc --traceResolution > resolution.log # 日志逐条展示:尝试了哪些路径、命中哪个、为何失败
// 常见病因:baseUrl/paths 配错,导致编译器扫描大量无关目录 { "compilerOptions": { "traceResolution": true } }
背景:某 import 一直慢。操作:开 traceResolution 查日志。结果:发现因 paths 没配对,编译器回退去 node_modules 层层搜。解读:解析阶段拖慢会连累整个 Program 建立。变式:listFiles 可列出实际参与编译的文件清单,揪出被误包含的大文件。
这张图把"性能瓶颈来源"分层:

checker 报错含位置、原因码(error TSxxxx)、相关类型。学会读就能快速定位。
// 报错:Type 'string' is not assignable to type 'number'. (TS2322) const x: number = "a"; // 读法:TS2322 = 赋值不兼容;左侧期望 number,右侧给 string
skipLibCheck 跳过的正是第三方 .d.ts 内部的这类检查,能大批提速而不影响你自己的代码——前提是你信任那些声明。
{ "compilerOptions": { "incremental": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo" } }
开启后 tsc 把上次的符号图缓存到 .tsbuildinfo,二次运行只重查改动文件及其依赖。对"改一行等半天"的项目立竿见影。注意:缓存文件要进 .gitignore 或放 node_modules,别提交。
单 tsconfig 覆盖整仓时,checker 要做"全程序"分析,任一文件改动都可能触发大范围重算。用项目引用把仓库切成多个 composite 子项目,改动只影响当前子项目与其下游。
// 根 tsconfig 用 references 指向子项目 { "files": [], "references": [ { "path": "./packages/core" }, { "path": "./packages/ui" } ] } // 每个子项目开启 composite // packages/core/tsconfig.json: { "compilerOptions": { "composite": true } }
背景:大仓里改 core,单配置下整仓重查。操作:拆项目引用 + tsc -b。结果:只重建 core 及依赖它的 ui。解读:这是把"检查单元"从"全仓"降到"子包",增量缓存的颗粒度变细,是第九章工程层提速的核心。
把常见瓶颈与对应开关做成可照做的清单:
症状:保存后整体卡顿 → 查 Instantiations 是否过高:收敛深层泛型/递归条件类型 → 查 Files 数量:include 是否误含 node_modules 或 dist 症状:import 解析慢 → 开 traceResolution,看是否 paths 没配对导致回退全扫 → 用 listFiles 确认实际参与编译的文件 症状:首次慢、二次快 → 已享受 incremental 缓存,保持 .tsbuildinfo 不被清 → CI 里缓存该文件,避免每次从零 症状:第三方声明检查慢 → skipLibCheck 跳过 node_modules .d.ts(不影响你的代码)
背景:性能问题五花八门,但根因就那几类。操作:按症状对号入座。结果:少绕路。解读:清单的意义是把"先量再改"流程化,新人也能按图索骥,不必每次都从诊断命令啃起。
常见 error code 记住几个,能省大量读堆栈的时间:
TS2322 赋值类型不兼容(右不能赋左) TS2345 实参类型不匹配(调用处) TS2339 访问不存在的属性(拼写或类型推断偏窄) TS2532 对象可能为 undefined(noUncheckedIndexedAccess 触发) TS2554 调用处参数数量不对
背景:同类型错误反复出现时,记 code 比读整句快。操作:把高频 code 贴团队 wiki。结果:review 时一眼识别根因。解读:error code 是 checker 给的"错误分类标签",熟悉它等于有了快速索引——和运行时异常的 errno 同理。
性能优化第一原则:先量再改。我们见过团队盲开 skipLibCheck 以为提速,其实瓶颈在单个巨型泛型文件——那才是该收敛的地方。诊断工具(--extendedDiagnostics/traceResolution)十分钟能定位,比拍脑袋强。
当单仓多包时,全量检查慢的主因是"任何改动都重查整仓"。项目引用让每个子包有独立符号图。
// 子包 tsconfig.json { "compilerOptions": { "composite": true, "incremental": true }, "references": [{ "path": "../core" }] }
# 仓库根用 build 模式 npx tsc -b
输入输出:第一次建缓存后,改 web 包不会重查 core 的内部,只重查 web 与依赖它的下游。这和第四章构建集成、第九章性能策略同源,是"检查成本压到包级"的核心手段。注意 composite 要求 incremental,且每个子包要能独立产出 .d.ts。
isolatedModules 模拟打包器逐文件转译的约束,提前暴露那些"依赖跨文件类型信息"的写法,避免开发时绿灯、打包时翻车。
{ "compilerOptions": { "isolatedModules": true } }
开启后,以下写法会报错并提示修正:
// 重导出类型必须显式标 type,否则打包器逐文件转译会丢 export { SomeType } from "./x"; // 报错:疑似类型却当值重导出 export type { SomeType } from "./x"; // 正确 // 常量枚举跨文件不可用也会在此暴露
输入输出:开启后这类隐患在 tsc 阶段就红,而不是等到 esbuild/Vite 打包才因"类型被剥、引用丢失"而崩。解读:它把"转译器视角"的约束提前到检查期,和本节的"先诊断再修"一脉相承。
--extendedDiagnostics 看各阶段耗时,先定位traceResolution/listFiles 查解析与包含范围incremental 缓存符号图,skipLibCheck 跳第三方⚠️ 别一慢就全员开 skipLibCheck 了事。它可能掩盖真凶——你自己的复杂类型;先诊断确认瓶颈在第三方声明,再开它才合理。
💡 性能问题先跑 --extendedDiagnostics 看 Instantiations 和 Check time,数字会告诉你该收敛泛型还是该收窄 include 范围,少走弯路。