TypeScript monorepo 编译 12min 优化到 90s:Project Refs + SWC + Turbo

60 个包 / 200 万行 TS monorepo,tsc 全量 12min,增量 30-60s,VSCode 占 4GB 卡顿。一个月治理:Project References 分模块 + tsbuildinfo + SWC 编译 + tsc 仅类型检查 + Turbo 增量缓存 + 远程缓存 + tsserver 调优。最终 90s,增量 < 5s,CI 25min → 45s。

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%
- 大型重构敢做了(以前怕慢不敢动)

避坑清单

  1. Project References 必上,大 monorepo 必备
  2. composite: true 才能用 References + 增量
  3. skipLibCheck: true(99% 情况无副作用)
  4. tsc 只做类型检查,编译交给 SWC / esbuild
  5. Turbo + 本地 cache + 远程 cache 三件套
  6. noUncheckedIndexedAccess / exactOptionalPropertyTypes 很慢,慎用
  7. VSCode 打开子目录,不要打开根
  8. path alias 替代相对路径(更快解析)
  9. 测试用单独 tsconfig.test.json
  10. 定期 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
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Spring Boot 启动 60s 优化到 8s 实录:测量 → CDS → AOT 全路径

2026-5-19 13:07:25

技术教程

微服务 P99 8s 雪崩复盘:全链路超时预算 + 传递 + 重试治理

2026-5-19 13:11:33

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索