2024 年我们一个 TypeScript 前端单仓(monorepo)有 60 个包、200w 行 TS,tsc --build 一次要 12 分钟,CI 慢、本地开发更慢,VSCode 经常卡顿。投了一个月做编译性能治理,最终 tsc 全量 12min → 90s,增量 < 5s,VSCode 内存 4GB → 1.2GB。本文复盘 TypeScript 大型项目编译性能优化的完整路径,覆盖 Project References、tsbuildinfo、type-checking 分离、SWC/esbuild、tsserver 调优。
问题背景
仓库:pnpm + nx monorepo
- 60 个 package(apps/* + libs/*)
- 200 万行 TypeScript
- @types 依赖 80 个
- 单仓 tsconfig 互相 path 引用
性能问题:
- tsc --build 全量:12 分钟
- 增量改一个文件:30-60 秒
- VSCode tsserver 占用 4GB 内存
- 改文件后 VSCode 提示延迟 10 秒
- CI 跑 type-check + 编译 + 测试 25 分钟
定位:
$ time tsc --build --extendedDiagnostics
Files: 15234
Lines of Library: ...
Nodes of Library: ...
Files parsed: 15234
I/O Read time: 8.42s
Parse time: 45.31s
ResolveTypeReference time: 22.14s
Bind time: 38.92s
Check time: 412.34s ← 占 70%
Total time: 726s
Memory used: 3245MB
第 1 步:Project References(分模块编译)
// 根 tsconfig.json
{
"files": [],
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/utils" },
{ "path": "./packages/ui" },
{ "path": "./apps/web" },
{ "path": "./apps/admin" }
]
}
// packages/core/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true, // 必须,生成 .d.ts + tsbuildinfo
"declaration": true,
"declarationMap": true,
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/.tsbuildinfo"
},
"include": ["src/**/*"],
"references": [
{ "path": "../utils" } // 依赖
]
}
// packages/ui/tsconfig.json 依赖 core
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist"
},
"references": [
{ "path": "../core" }
]
}
// 执行
$ tsc --build --verbose
[12:00:01] Project '@my/utils' is out of date because output file '...' does not exist
[12:00:03] Building project '@my/utils'...
[12:00:05] Project '@my/core' is out of date
[12:00:08] Building project '@my/core'...
[12:00:11] Project '@my/ui' is out of date
[12:00:14] Building project '@my/ui'...
...
// 优势:
// 1. 每个包独立编译,生成 .d.ts
// 2. 改一个包,只重编它和下游
// 3. tsbuildinfo 记录上次结果,支持增量
// 效果:全量从 12min → 4min,增量改一个 file → 5s
第 2 步:tsbuildinfo + skipLibCheck
// tsconfig.base.json
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./node_modules/.cache/tsbuildinfo",
// 跳过 .d.ts 类型检查(大幅提速,节点模块的 .d.ts 都是预编译过的)
"skipLibCheck": true,
// 跳过默认 lib(项目里不用的)
"skipDefaultLibCheck": true,
// ES 模块,不用 commonjs(更快的解析)
"module": "ESNext",
"moduleResolution": "Bundler",
// 关闭装饰器元数据(用不上的话)
"emitDecoratorMetadata": false,
// 关 sourceMap(线上用 esbuild/swc 自己生成)
"sourceMap": false,
// 性能选项
"preserveWatchOutput": true,
"assumeChangesOnlyAffectDirectDependencies": true,
// 不严格 isolatedModules(但配合 SWC 可开)
"isolatedModules": true
}
}
// 效果:增量编译 5s → 2s
第 3 步:类型检查 vs 编译分离
核心思路:
- tsc 只做类型检查(不输出 js)
- SWC / esbuild 做编译(不做类型检查)
# 编译用 SWC(Rust 实现,比 tsc 快 20-70 倍)
$ pnpm add -D @swc/core @swc/cli
# .swcrc
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true,
"decorators": false
},
"target": "es2020",
"transform": {
"react": {
"runtime": "automatic"
}
}
},
"module": {
"type": "es6"
}
}
# 编译
$ swc src -d dist --strip-leading-paths
Successfully compiled 1234 files in 3.45s ← 之前 tsc 要 60 秒
# 类型检查(并行)
$ tsc --noEmit --build
# package.json
{
"scripts": {
"build": "swc src -d dist && tsc --noEmit",
"build:fast": "swc src -d dist", // CI 主链路
"typecheck": "tsc --noEmit --build", // pre-commit / CI 单独跑
"watch": "swc src -d dist --watch"
}
}
第 4 步:Turbo Build(增量 + 缓存)
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**/*.ts", "src/**/*.tsx", "tsconfig.json"],
"outputs": ["dist/**", ".next/**"],
"cache": true
},
"typecheck": {
"dependsOn": ["^build"],
"inputs": ["src/**/*.ts", "tsconfig.json"],
"outputs": [".tsbuildinfo"],
"cache": true
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**/*.ts", "test/**/*.ts"],
"cache": true
}
}
}
# 第一次跑(冷启动)
$ pnpm turbo run build typecheck
✓ @my/utils:build (3.2s)
✓ @my/core:build (8.4s)
✓ @my/ui:build (12.1s)
✓ @my/web:build (45.6s)
Tasks: 60 successful, 60 total
Time: 1m 30s
# 第二次跑(全部 cache hit)
$ pnpm turbo run build
✓ @my/utils:build [cached]
✓ @my/core:build [cached]
✓ @my/ui:build [cached]
Tasks: 60 successful, 60 total
Cached: 60 cached, 60 total
Time: 800ms
# 改一个文件,只重新编一个包 + 依赖
$ # 改 packages/utils/src/foo.ts
$ pnpm turbo run build
✓ @my/utils:build (3.2s)
✓ @my/core:build (8.4s) # utils 是 core 的依赖,重编
✓ @my/ui:build [cached] # 没改不重编
Tasks: 60 successful, 60 total
Cached: 58 cached, 60 total
Time: 12s
第 5 步:远程缓存(团队共享)
# Turbo 远程缓存(Vercel 免费提供,或自建)
$ pnpm turbo login
$ pnpm turbo link
# CI 跑过的结果,本地直接拉
$ pnpm turbo run build
✓ @my/web:build [cached, REMOTE] # 远程缓存命中
Time: 5s
# 自建 remote cache(turbo-cache-server)
$ docker run -p 3000:3000 \
-e TURBO_TOKEN=secret \
ducktors/turborepo-remote-cache
# 团队 .turborc
{
"remoteCache": {
"endpoint": "https://turbo-cache.internal.com",
"token": "secret"
}
}
# 效果:首次 PR build 5s(因为同事已经 build 过,远程 hit)
第 6 步:tsserver 内存优化(VSCode)
// .vscode/settings.json
{
// 限制 tsserver 内存
"typescript.tsserver.maxTsServerMemory": 8192,
// 关掉太重的特性
"typescript.disableAutomaticTypeAcquisition": true,
// 减少打开文件数
"typescript.tsserver.watchOptions": {
"watchFile": "useFsEvents",
"watchDirectory": "useFsEvents",
"fallbackPolling": "dynamicPriority"
},
// 关闭 inline hint(默认 off,但有些主题会开)
"typescript.inlayHints.parameterNames.enabled": "none",
// 用 Project References,不要 forced single-project mode
"typescript.preferences.includePackageJsonAutoImports": "off",
// ESLint 用单独进程
"eslint.run": "onSave",
"eslint.options": {
"cache": true,
"cacheLocation": ".eslintcache"
}
}
// 巨型 monorepo:打开子项目而非根目录
$ code apps/web # 比 code . 内存少一半
第 7 步:tsconfig 优化技巧
{
"compilerOptions": {
// 严格但不过度
"strict": true,
// 这些个 strict 选项很慢,看实际需要
"noUncheckedIndexedAccess": false, // 慢
"exactOptionalPropertyTypes": false, // 慢
// path 别名替代相对路径(更快解析)
"baseUrl": ".",
"paths": {
"@core/*": ["packages/core/src/*"],
"@utils/*": ["packages/utils/src/*"]
},
// 缩小 include 范围(不要 ** 扫整个仓库)
// 在 include / exclude 写明确
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"**/node_modules",
"**/dist",
"**/__tests__",
"**/*.test.ts",
"**/*.spec.ts"
]
}
// 测试单独 tsconfig
// tsconfig.test.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"]
},
"include": [
"src/**/*.test.ts",
"src/**/*.spec.ts"
]
}
CI 优化
# GitHub Actions
jobs:
build:
runs-on: ubuntu-latest-8-core # 8 核机器
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
# Turbo 缓存(GitHub Actions Cache)
- uses: actions/cache@v4
with:
path: |
.turbo
node_modules/.cache
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
- run: pnpm install --frozen-lockfile
# 并行:type check + build + test
- name: Build & test
run: pnpm turbo run typecheck build test --concurrency=8
# 上传 turbo 远程缓存
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
# 优化前:25 分钟
# 优化后:3 分钟(首次)/ 45 秒(cache 命中)
优化效果总览
指标 优化前 优化后
==========================================================
tsc 全量编译 12min 90s
tsc 增量编译 30-60s < 5s
SWC 编译(独立) -- 3s(60 包)
turbo 全部 cache 命中 -- 800ms
VSCode 内存 4GB 1.2GB
VSCode 提示延迟 10s < 1s
CI 编译 + 测试 25min 3min(首次)
45s(cache 命中)
业务影响:
- 开发体验大幅改善,提示秒级
- PR 反馈速度从 25min 缩到 3min,合入快
- CI 资源成本节约 80%
- 大型重构敢做了(以前怕慢不敢动)
避坑清单
- Project References 必上,大 monorepo 必备
- composite: true 才能用 References + 增量
- skipLibCheck: true(99% 情况无副作用)
- tsc 只做类型检查,编译交给 SWC / esbuild
- Turbo + 本地 cache + 远程 cache 三件套
- noUncheckedIndexedAccess / exactOptionalPropertyTypes 很慢,慎用
- VSCode 打开子目录,不要打开根
- path alias 替代相对路径(更快解析)
- 测试用单独 tsconfig.test.json
- 定期 tsc --extendedDiagnostics 查看耗时分布
总结
TypeScript 大型项目的编译性能不是单点问题,要从工具链整体看:Project References 分模块、SWC 替代 tsc 编译、Turbo 提供增量和缓存、远程缓存团队共享、tsserver 内存调优。最大的认知改变:tsc 慢的根本原因不是它做编译,是它把类型检查和编译耦合在一起。把"类型检查"和"emit JS"解耦,用 SWC 编译 + tsc --noEmit 检查,你能拿到 20-70 倍的速度。最被低估的是 Turbo 的远程缓存,团队 5 人开发,同事 build 过的产物你直接复用,本地几乎没编译。最容易踩的是 tsconfig 的 include 配错,**/*.ts 扫了 node_modules,VSCode 卡到怀疑人生 — 一定要明确 include 范围。最后,2024 年还在用纯 tsc 编译大型 monorepo 的项目都该升级,SWC + Turbo + Project References 是黄金组合。
—— 别看了 · 2026