TypeScript 构建性能优化:增量编译、缓存与工具链选型

系统覆盖 TypeScript 项目构建与类型检查的性能优化:构建链路拆解(类型检查 vs 转译)、tsc 全量 vs 增量、incremental 与 composite 项目引用、isolatedModules 与转译器(esbuild/swc/tsup)、monorepo 缓存(Turborepo)、ts-loader/babel 对比、skipLibCheck 与路径映射影响,以及构建性能诊断与优化清单。

引言

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 转译

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 三种转译器对比

工具语言速度类型检查用途
esbuildGo极快无打包/转译
swcRust极快无打包/转译(Next.js 默认)
tsupesbuild极快可选(单独跑 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
webpackswc-loader + fork-ts-checker
Viteesbuild 开发 + tsc 构建
跳过第三方检查skipLibCheck
类型检查tsc –noEmit –incremental

一句话记忆:TS 构建优化总纲是「类型检查与转译分离」——转译交给 esbuild/swc(快),类型检查走增量(incremental)与项目引用(composite + tsc -b);isolatedModules 保证转译器安全、Turborepo 缓存未变任务;skipLibCheck 跳过第三方、paths 别名各层配置;诊断先量化再定位,CI 记录时长防回归——构建从「每次全量几十秒」变成「增量秒级 + 检查独立保底」。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「typescript」更多文章

  1. TypeScript 错误处理:Result 模式、类型化错误与错误边界实战
  2. TypeScript 测试策略:单元测试、类型测试与测试替身实战
  3. TypeScript 库作者指南:声明文件、API 演进与包发布