9.1 编译性能优化策略


9.1 编译性能优化策略

这节在地图里是"让契约跑得动"的工程层。第六章讲了慢在类型检查阶段,这一节给具体提速开关与组织结构。契约再好,若保存要等十秒,团队也会偷偷关掉它——所以速度本身就是正确性的保障。

第一招:incremental 缓存符号图

// tsconfig.json { "compilerOptions": { "incremental": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo" } }

第二次运行只重查改动文件及其依赖。对"改一行全量重查"的项目,这是性价比最高的一招。注意 tsBuildInfoFile 要放 node_modules 或进 gitignore,别提交缓存。

第二招:项目引用拆分大仓

references 让子项目各自有独立编译信息,互相只消费 .d.ts,不整体重查。

// 根 tsconfig.json( solution 模式) { "files": [], "references": [ { "path": "./packages/core" }, { "path": "./packages/api" } ] }
# 只构建变更的子项目 npx tsc -b # build 模式,按依赖图增量构建

api 改了不影响 core 的重新检查,-b 模式按图调度。这把"一次检查全仓"变成"只查动过的包"。

第三招:skipLibCheck 跳第三方

{ "compilerOptions": { "skipLibCheck": true } }

第三方 .d.ts 内部不查,能省大量时间。前提是信任那些声明(热门库基本可靠)。第六章强调过:别盲目开,先确认瓶颈在第三方声明(用 --extendedDiagnosticsCheck time)。

第四招:隔离 transpileModule 做编辑器反馈

编辑器实时标红若卡,可让语言服务用 transpileModule 模式(只转译不全局检查)做最快反馈,全局检查交给保存时或 CI。

// .vscode/settings.json { "typescript.tsserver.useSeparateSyntaxServer": true }

这张图把四招对应的"提速层面"分层:

09-01-fig01

实战:诊断后再决定开哪招

# 1. 看耗时分布 npx tsc --extendedDiagnostics # 2. 看实际编译哪些文件(揪误包含的大文件) npx tsc --listFiles | wc -l # 3. 看模块解析路径 npx tsc --traceResolution > log.txt

背景:项目检查要 40 秒。操作:先跑诊断。结果:发现 includedist 也包进来了。解读:收窄 include 比上任何缓存都直接。变式:超大单文件用 @ts-nocheck 临时隔离(应急,别常驻)。

工程取舍:速度 vs 严格度不可兼得是伪命题

很多人以为"快就得关检查"。其实 incremental/skipLibCheck/项目引用都是"少做无用功",不降低对你自己代码的严格度。真正该警惕的是用 any 或关 strict 换速度——那是砍契约,不是提速。

实战:用 ts-expect-error 临时压住单点错误

迁移老项目时,全量错误太多,可用 // @ts-expect-error 标记"已知待修"的单点,让 CI 允许通过、同时防止误删。

// @ts-expect-error 旧逻辑类型未补,issue #123 跟进 const total = legacyCalc(a, b) as number; // 若以后这行真的修对了,@ts-expect-error 会因"无错可压"而自己报错, // 提醒你删掉注释——这是它比 @ts-ignore 安全的地方

输入输出:标了 expect-error 的行若真的没错误了,编译器反而报"未使用的 ts-expect-error",逼你清理。对比 @ts-ignore 永远沉默,expect-error 更适合"遗留债务"场景。注意:只能压单点、带 issue 编号,别用它当 as 的替代品。

实战:用 exclude 把生成物与测试隔离出检查

检查慢常因把 distcoverage、测试桩也拉进 Program。用 exclude 把它们挡在门外。

{ "compilerOptions": { "rootDir": "src" }, "include": ["src"], "exclude": ["node_modules", "dist", "coverage", "**/*.test.ts", "src/**/*.stories.ts"] }

输入输出:排除后,tsc 建立的 Program 只含 src 真源码,文件数大幅下降,Check time 随节点数线性下降。配合 --listFiles 可核对实际参与的文件清单,避免"以为排除了其实没排除"。解读:这是性价比极高的第一刀——先让检查范围正确,再谈缓存与增量。

实战:用诊断输出量化"收敛泛型"的成效

第六章说过泛型过度实例化是头号瓶颈,这里给出量化验证手法,改完能看出是否真有效。

# 改前 npx tsc --extendedDiagnostics 2>&1 | grep -E "Instantiations|Check time" # 例如:Instantiations: 520000 / Check time: 28.4s # 收敛一个被滥用的大泛型后 npx tsc --extendedDiagnostics 2>&1 | grep -E "Instantiations|Check time" # 例如:Instantiations: 90000 / Check time: 6.1s

背景:有人写了 DeepMerge<AllConfig, Partial<AllConfig>> 被几百处实例化。操作:改用浅合并 + 显式字段。结果:Instantiations 掉一个数量级,Check time 同步下降。解读:性能优化不能凭感觉,"改前改后数字对比"才是闭环——这和本节的"先度量再修"完全一致。

第五招:verbatimModuleSyntax 减少模块图负担

开启 verbatimModuleSyntax 后,类型导入必须显式 import type,编译器不必再"猜"某个 import 是值还是类型,能更早裁剪模块依赖图,对超大项目有可观提速。

{ "compilerOptions": { "verbatimModuleSyntax": true } }
import type { User } from "./models"; // 类型:编译器知其无运行时副作用 import { saveUser } from "./api"; // 值:照常保留

背景:大型项目里"到底是值还是类型"的判断要扫整个图,开销累积明显。操作:开启开关 + 用 import type。结果:模块图更干净,检查更快。解读:它和第四章的语义收紧同源,顺带给速度——又一次"严格度与性能同向"的例证,而非取舍。

第六招:isolatedModules 配合单文件转译

isolatedModules 要求每个文件可独立转译(不依赖跨文件类型信息),这恰好让 esbuild/transpileModule 等单文件工具能安全处理,避免了"为类型信息必须全局分析"的开销。

{ "compilerOptions": { "isolatedModules": true } }
// 开启后,重新导出类型必须显式 import type export type { User } from "./models"; // OK // export { User } from "./models"; // 报错:User 是类型,需 type 前缀

背景:想用 Vite/esbuild 做极快单文件转译。操作:开 isolatedModules 并规范类型导入。结果:转译层不必全局分析,构建分离更彻底(呼应第四章构建集成)。解读:它对"检查/转译职责分离"是强约束,间接让检查范围更可控。

第七招:--watch 与编辑器语言服务的智能增量

长期运行的检查进程比"每次冷启动"快得多,因为它把符号图常驻内存,watch 模式只增量重查改动。

# 开发时启动常驻检查,文件改动即增量重查 npx tsc --watch --noEmit # 编辑器语言服务本就是常驻进程,故实时标红比命令行冷跑快
// 让语言服务独立语法服务器,先极速标红、再后台全量检查 { "typescript.tsserver.useSeparateSyntaxServer": true }

背景:冷启动要建立整个 Program(扫描所有文件),耗时可观。操作:常驻 watch/语言服务。结果:后续改动只增量。解读:常驻进程把"建图成本"摊到启动一次,后续每次改动边际成本极低——和 incremental 缓存是两条互补路径。

第八招:CI 缓存 .tsbuildinfo 与依赖

本地增量靠缓存文件,CI 每次全新拉取会丢缓存。把 .tsbuildinfonode_modules 在 CI 间缓存,让"首次检查"也享受增量。

# CI 配置(伪代码) cache: - node_modules - node_modules/.tmp/tsconfig.tsbuildinfo steps: - run: npm ci - run: npx tsc -b # 有缓存则只查变更

背景:CI 上"每次从零"让大仓检查动辄几分钟。操作:缓存构建信息文件。结果:CI 也走增量,时长从分钟级降到秒级。解读:本地快、CI 慢的常见根因就是"缓存没带过去",把 tsBuildInfoFile 纳入缓存即对齐两端体验(详见第四章 CI 集成)。

实战:八招的优先级排序

不是八招全上,按"性价比与风险"排个序,先落低风险高收益的:

第一梯队(低风险、立竿见影): 1. exclude 收窄 include 范围 2. incremental 缓存符号图 3. skipLibCheck 跳第三方(先诊断确认) 第二梯队(结构改造,需规划): 4. 项目引用 + tsc -b 拆大仓 5. CI 缓存构建信息 第三梯队(语义收紧,顺带提速): 6. verbatimModuleSyntax + import type 7. isolatedModules 8. 收敛过度泛型(第六章诊断驱动)

背景:有人一上来就拆项目引用,工程量大还容易出错。操作:先吃第一梯队三招。结果:多数项目到这就够了。解读:优化讲"先摘低垂果实",架构级改造留到有明确瓶颈时再做。

本节要点回顾

  • incremental 缓存符号图,二次只查改动
  • 项目引用 + tsc -b 让大仓按包增量
  • skipLibCheck 跳第三方,先诊断确认瓶颈
  • 速度是正确性保障,别用关检查换速度

⚠️ 别把"编译慢"直接归咎于 TS 本身。多数情况是 include 过宽、单文件过大、或重复全量检查;先诊断再优化,别轻易用 any 妥协。

💡 大仓项目优先上"项目引用 + tsc -b",比全局 incremental 更准——它按包粒度调度,改动一个包不会触发全仓重查。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U