引言
TS 项目越建越大,tsc 全量编译动辄几十秒甚至几分钟,CI 里「每次全量编译」把开发节奏拖垮。优化的核心认知是:TS 的构建可以做「类型检查」和「转译」分离——类型检查(tsc 的慢活)和代码转译(去掉类型的快活)本来就不必绑定。这带来了 incremental、esbuild/swc、tsup、monorepo 缓存等一系列手段。
本文系统讲 TS 构建性能:先拆解构建链路(为什么慢),再讲 tsconfig 的增量与项目引用(incremental/composite),深入 isolatedModules 与转译器选型(esbuild/swc/tsup),覆盖 monorepo 缓存、webpack 集成、skipLibCheck 与路径别名影响,最后给出性能诊断方法与优化清单。
前置:/typescript-strict-config/(tsconfig 详解)、/typescript-project-architecture-tsconfig/(项目架构)、/typescript-sdk-package-publishing/(库发布与构建)。
目录
- 1. 构建链路拆解:类型检查 vs 转译
- 2. incremental:让 tsc 记住上一次
- 3. composite 与项目引用
- 4. isolatedModules:让转译器能独立编译
- 5. 转译器选型:esbuild / swc / tsup
- 6. monorepo 缓存与并行构建
- 7. webpack/Vite 集成
- 8. skipLibCheck 与路径映射的影响
- 9. 性能诊断方法
- 10. 速查表
- 延伸阅读
1. 构建链路拆解:类型检查 vs 转译
1.1 tsc 一次做了两件事
tsc 编译 = 类型检查(慢)+ 转译(快)
类型检查:解析整个类型图、推导、报错 → O(项目大小),是大头
转译 :去掉类型、生成 JS → 相对快
优化思路:把两者拆开——
转译用快工具(esbuild/swc),类型检查单独跑(且尽量增量/缓存)
1.2 为什么全量 tsc 慢
1. 每次全量解析所有文件(无记忆)
2. 类型检查是全程序分析(依赖图)
3. 装饰器/高级类型更慢
4. 大项目(数千文件)指数放大
1.3 优化总览
手段:
1. incremental:tsc 记住上次结果(增量)
2. 转译器替换:esbuild/swc(快 10-100 倍)
3. 项目引用:按依赖图只编译变更的包
4. monorepo 缓存:Turborepo 缓存未变任务
5. 类型检查独立:开发时跳过 typecheck,CI 再全量
一句话总结:TS 构建慢在「类型检查」,不在「转译」——把两者拆开,转译交给 esbuild/swc、类型检查走增量与缓存,是优化的总纲。
2. incremental:让 tsc 记住上一次
2.1 开启增量编译
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./node_modules/.cache/tsconfig.tsbuildinfo"
}
}
2.2 incremental 的作用
tsc 把「上一次的编译信息」存到 .tsbuildinfo
下次只重新检查「变更文件及其依赖」→ 大幅提速
适用:本机开发、重复 typecheck
局限:CI 每次是干净环境 → 缓存要持久化(CI cache 或 monorepo 缓存)
2.3 .tsbuildinfo 的管理
# .gitignore 里排除
node_modules/.cache/
*.tsbuildinfo
# CI 缓存 tsbuildinfo
- uses: actions/cache@v4
with:
path: node_modules/.cache
key: tsc-${{ hashFiles('src/**/*.ts') }}
一句话总结:incremental 让 tsc 增量检查、缓存编译信息——本机提速明显,CI 需持久化缓存(actions/cache)才有效。
3. composite 与项目引用
3.1 项目引用(Project References)
大项目拆成多个子项目:每个有自己的 tsconfig + 依赖声明
tsc 只构建「变更的子项目 + 依赖它的」→ 按依赖图增量
// tsconfig.base.json
{
"compilerOptions": {
"composite": true, // 子项目必须开
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": "src"
}
}
// apps/web/tsconfig.json —— 引用共享包
{
"compilerOptions": { "composite": true },
"references": [
{ "path": "../../packages/ui" },
{ "path": "../../packages/types" }
]
}
3.2 composite 的约束
composite 强制:declaration 开启、rootDir 明确、noEmit 不能开
构建顺序:tsc -b(build 模式)自动按 references 拓扑排序
# 构建所有引用项目(按依赖顺序)
tsc -b apps/web packages/ui packages/types
# 只构建变更的
tsc -b apps/web --watch
3.3 适用场景
1. 大型 Monorepo:包按依赖图增量编译
2. 前后端共享类型:类型包单独编译,引用方直接消费 d.ts
3. CI 中按 affected 构建
一句话总结:项目引用把大项目拆成依赖图,tsc -b 按拓扑增量构建变更的子项目——是大型 TS Monorepo 的标配。
4. isolatedModules:让转译器能独立编译
4.1 为什么需要 isolatedModules
esbuild/swc 按「单文件」转译(不做全程序类型分析)
某些 TS 语法需要「全局信息」才能正确转译:
- 重导出类型(import { T } / export { T })可能被误删
- 命名空间合并
isolatedModules 强制代码「可单文件编译」,避免转译器误删类型
{
"compilerOptions": {
"isolatedModules": true,
"verbatimModuleSyntax": true // 显式区分 type/值导入
}
}
4.2 verbatimModuleSyntax 的类型导入
import type { UserDTO } from './types' // 明确是类型 → 转译器安全删除
import { getUser } from './api' // 值是值 → 保留
export type { UserDTO } // 类型重导出,安全
4.3 常见违规
// 反例:值导入误当类型用,转译器可能误判
import { UserDTO } from './types' // UserDTO 是类型 → 用 import type
// 反例:export 类型时混在值里
export { UserDTO } from './types' // 应 export type { UserDTO }
一句话总结:isolatedModules 保证代码「单文件可转译」,verbatimModuleSyntax 用 import type/export type 明确类型边界——是切换 esbuild/swc 的前置安全开关。
5. 转译器选型:esbuild / swc / tsup
5.1 三种转译器对比
| 工具 | 语言 | 速度 | 类型检查 | 用途 |
|---|---|---|---|---|
| esbuild | Go | 极快 | 无 | 打包/转译 |
| swc | Rust | 极快 | 无 | 打包/转译(Next.js 默认) |
| tsup | esbuild | 极快 | 可选(单独跑 tsc) | 库打包 |
5.2 用 esbuild 转译、tsc 检查
# 开发:esbuild 转译(秒级)
esbuild src/index.ts --bundle --outfile=dist/index.js --platform=node
# 类型检查:单独跑(增量)
tsc --noEmit --incremental
// build.ts(脚本化)
import { build } from 'esbuild'
await build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/index.js',
platform: 'node',
sourcemap: true,
target: 'es2022',
})
5.3 tsup 打包库
# tsup 默认用 esbuild,可配置 typecheck
npx tsup src/index.ts --dts --sourcemap
// tsup.config.ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true, // 生成 .d.ts(内部用 tsc)
sourcemap: true,
clean: true,
})
5.4 转译器选型总结
开发构建/打包 → esbuild(Go,最快)或 swc(Rust,Next.js 默认)
库发布 → tsup(esbuild + dts)
类型检查 → 永远用 tsc(转译器不做类型检查)
关键:转译器负责「快」,tsc 负责「准」,两者分离
一句话总结:esbuild/swc 负责「快转译」,tsc 单独负责「准检查」;tsup 把 esbuild 打包 + tsc 生成 d.ts 组合成库发布工具。
6. monorepo 缓存与并行构建
6.1 Turborepo 缓存未变任务
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"],
"cache": true
},
"typecheck": { "cache": true }
}
}
pnpm turbo build # 未变包命中缓存
pnpm turbo typecheck # 类型检查也缓存
6.2 缓存键 = 输入哈希
缓存键 = 源码哈希 + 配置 + 依赖锁定文件
未变 → 直接取上次产物(秒级)
远程缓存(Turborepo Remote Cache)→ CI 与本地共享
6.3 并行构建注意
1. 依赖顺序:dependsOn ^build 保证先构建依赖
2. 内存限制:并行度过高会 OOM → 限制 --concurrency
3. 缓存失效:环境变量/路径别名变化会清缓存
一句话总结:Turborepo 用「输入哈希 + 产物缓存」让未变包秒级跳过构建与类型检查;并行构建配 dependsOn 保顺序、限并发防 OOM。
7. webpack/Vite 集成
7.1 webpack + ts-loader vs babel-loader
ts-loader:每次全量类型检查(慢)
babel-loader:只转译不检查(快),类型检查交给 fork-ts-checker
esbuild-loader / swc-loader:更快
最佳实践:
babel-loader / swc-loader(转译)+ fork-ts-checker(后台类型检查)
7.2 Vite 的 TS 处理
Vite 开发:esbuild 转译(秒级热更新),不做类型检查
类型检查:单独 script 跑 vue-tsc / tsc --noEmit
{
"scripts": {
"dev": "vite", // esbuild 转译
"build": "tsc --noEmit && vite build" // 先检查再构建
}
}
7.3 分离开发/生产的检查策略
开发:转译器秒级(跳过 typecheck)→ 体验快
CI/发布:tsc --noEmit 全量检查 → 质量保底
折中:本地用 vite-tsc / fork-ts-checker 后台检查
一句话总结:webpack 用 swc/esbuild-loader + fork-ts-checker 分离转译与检查;Vite 开发走 esbuild、构建时先 tsc –noEmit。
8. skipLibCheck 与路径映射的影响
8.1 skipLibCheck
{
"compilerOptions": { "skipLibCheck": true }
}
作用:跳过 .d.ts 文件的类型检查(只检查业务代码)
收益:显著提速(node_modules 类型不重复查)
风险:node_modules 内类型错误不报 → 一般可接受(第三方错误非你可控)
推荐:生产项目开启
8.2 路径映射(paths)的构建影响
// tsconfig paths
{
"compilerOptions": {
"baseUrl": ".",
"paths": { "@app/*": ["src/*"] }
}
}
影响:
1. 转译器(esbuild/swc)需插件才能解析 paths(别名)
2. 打包器(webpack/vite)各自配 resolve.alias
3. 生成 .d.ts 时 paths 要能正确映射(否则类型引用错)
实践:paths 只在源码层用,产物层用相对路径/包名
8.3 类型检查加速的其他开关
{
"compilerOptions": {
"skipLibCheck": true,
"noEmit": true, // 只检查不产出(CI typecheck)
"incremental": true,
"types": [] // 只加载需要的 @types(减少全局)
}
}
一句话总结:skipLibCheck 跳过第三方类型检查提速、paths 别名需转译器/打包器各配解析;typecheck 用 noEmit + incremental,types 精简加载。
9. 性能诊断方法
9.1 测量时间
# 量化
time npx tsc --noEmit
time pnpm turbo build --dry-run # 看缓存命中
9.2 定位瓶颈
1. tsc 慢 → 检查 skipLibCheck / incremental / 项目引用
2. 打包慢 → 检查是否重复类型检查(ts-loader)→ 换 swc/babel
3. 特定文件慢 → 装饰器/巨型类型/循环引用
4. 缓存不命中 → 检查缓存键(环境变量/路径)
9.3 监控与回归
CI 记录构建/typecheck 时长 → 超阈值告警
定期清理:无引用文件、any 堆积、巨型 d.ts
用 trace(tsc --traceResolution / --generateTrace)深挖
一句话总结:诊断先量化(time/缓存命中率),再按「tsc vs 打包 vs 缓存」定位;用 trace 深挖 + CI 时长监控防回归。
10. 速查表
| 需求 | 方案 |
|---|---|
| 增量检查 | incremental + tsbuildinfo |
| 依赖图构建 | composite + 项目引用 |
| 快转译 | esbuild / swc |
| 库打包 | tsup(esbuild + dts) |
| 单文件安全 | isolatedModules + verbatimModuleSyntax |
| Monorepo 缓存 | Turborepo |
| webpack | swc-loader + fork-ts-checker |
| Vite | esbuild 开发 + tsc 构建 |
| 跳过第三方检查 | skipLibCheck |
| 类型检查 | tsc –noEmit –incremental |
一句话记忆:TS 构建优化总纲是「类型检查与转译分离」——转译交给 esbuild/swc(快),类型检查走增量(incremental)与项目引用(composite + tsc -b);isolatedModules 保证转译器安全、Turborepo 缓存未变任务;skipLibCheck 跳过第三方、paths 别名各层配置;诊断先量化再定位,CI 记录时长防回归——构建从「每次全量几十秒」变成「增量秒级 + 检查独立保底」。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。