TypeScript 给大型前端项目带来了类型安全,但也常常带来一个令人头疼的问题:编译越来越慢。当项目膨胀到数百个文件、多个子包相互依赖时,一次 tsc 可能消耗几十秒甚至几分钟,严重拖慢开发与 CI 流程。本文将围绕三种主流优化手段——项目引用(Project References)、增量编译(Incremental Compilation) 与 SWC 转译——说明它们的原理、适用场景和落地方式,帮助你系统性地解决编译速度问题。
为什么 TypeScript 编译会变慢
在动手优化之前,先理解瓶颈来源会更有针对性。常见原因包括:
- 全量类型检查:每次运行
tsc都会重新解析并检查所有文件,没有缓存。 - 单体项目结构:所有代码放在一个
tsconfig.json下,任何改动都会触发整个项目的重新检查。 - 类型依赖链过长:一个基础类型文件的改动,会级联影响大量下游文件。
- 编译与转译耦合:
tsc同时承担类型检查和代码生成,而代码生成本身并不需要那么重的处理。
针对这些原因,下面三种方案分别从结构拆分、缓存复用和职责分离三个角度切入。
一、项目引用:把大项目拆成可独立编译的单元
项目引用(Project References)是 TypeScript 官方提供的多包编译方案。它允许你把项目拆成多个子项目,每个子项目有自己的 tsconfig.json,并通过 references 字段声明依赖关系。
基本配置
假设项目分为 core、utils、app 三个包:
// packages/core/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
// packages/app/tsconfig.json
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"references": [
{ "path": "../core" },
{ "path": "../utils" }
],
"include": ["src"]
}
关键点:
- 被引用的项目必须开启
composite: true,并生成声明文件(declaration: true)。 - 使用
tsc --build(或tsc -b)来编译,它会根据依赖图自动决定编译顺序。 - TypeScript 会为每个子项目生成
.tsbuildinfo文件,记录编译状态,未改动的子项目会被跳过。
收益与代价
项目引用的最大价值在于增量边界清晰:修改 app 不会导致 core 重新编译。在 monorepo 中配合 pnpm workspace 或 npm workspaces 使用时效果尤其明显。
代价是配置复杂度上升,需要合理划分包边界。如果拆分过细,反而会因为大量小项目而增加管理成本。建议按业务域或复用层次划分,而不是按文件类型划分。
二、增量编译:让 tsc 记住上次的结果
增量编译是最容易落地的优化。它通过 .tsbuildinfo 缓存上次编译的文件签名与依赖信息,下次编译时只处理发生变化的文件及其影响范围。
开启方式
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./node_modules/.cache/tsconfig.tsbuildinfo"
}
}
几个实践要点:
- 缓存文件位置:默认生成在
outDir或项目根目录,建议统一放到node_modules/.cache,避免污染仓库,也方便在 CI 中做缓存。 - 与
composite的关系:开启composite会自动启用增量编译,因此项目引用场景下无需重复配置。 - CI 中的缓存:在 GitHub Actions 等环境中,缓存
.tsbuildinfo文件可以显著缩短流水线时间,但要确保缓存 key 与源码、依赖版本关联,避免脏缓存。
- uses: actions/cache@v4
with:
path: node_modules/.cache
key: tsbuildinfo-${{ hashFiles('**/tsconfig.json', 'pnpm-lock.yaml') }}
增量编译对重复编译场景收益最大,比如本地开发中的多次 tsc --watch,或 CI 中基于缓存的二次构建。但如果是全新环境、冷启动编译,它无法带来提升。
三、SWC:把转译从 tsc 中剥离
前面两种方案优化的都是 tsc 本身,而 SWC 走的是另一条路:用 Rust 编写的超快编译器替代 tsc 做代码转译,把类型检查交给独立的 tsc --noEmit。
为什么快
SWC 基于 Rust,天然支持多线程,且不做类型检查——它只负责把 TypeScript/JSX 转成目标 JavaScript。在大型项目中,转译速度通常是 tsc 的 10 到 20 倍。
在项目中接入
以 Vite 为例,安装插件即可:
npm install -D @vitejs/plugin-react-swc
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
export default defineConfig({
plugins: [react()],
})
如果使用 Next.js,可以在 next.config.js 中启用:
module.exports = {
experimental: {
swcPlugins: [],
},
swcMinify: true,
}
对于纯 tsc 项目,可以用 @swc/cli:
npx swc src -d dist --config-file .swcrc
类型检查怎么办
SWC 不检查类型,所以需要单独运行:
{
"scripts": {
"build": "swc src -d dist",
"typecheck": "tsc --noEmit"
}
}
在 CI 中把 typecheck 作为独立步骤,与构建并行执行,既保证了类型安全,又不拖慢产物生成。
适用边界
SWC 并非万能。它不支持 const enum 的某些跨文件行为、部分装饰器元数据场景,以及依赖类型信息做代码生成的库(如某些 NestJS 场景)。在这些情况下,要么保留 tsc,要么使用 ts-jest/babel 等替代方案。建议在引入前跑一遍完整测试。
组合实践:一套可落地的方案
三种手段并不互斥,实际项目中往往组合使用:
- 结构层面:用项目引用把 monorepo 拆成若干可独立编译的包。
- 缓存层面:为每个包开启增量编译,并在 CI 中缓存
.tsbuildinfo。 - 构建层面:开发与生产构建使用 SWC 转译,类型检查作为独立任务运行。
一个典型的脚本编排如下:
{
"scripts": {
"dev": "vite",
"build": "swc src -d dist && tsc --noEmit",
"build:ci": "tsc -b --verbose && swc src -d dist"
}
}
此外,还有一些辅助手段值得关注:
skipLibCheck: true:跳过node_modules中声明文件的类型检查,通常能省下可观的時間。isolatedModules: true:让每个文件可独立转译,与 SWC、Babel 等工具的行为保持一致。- 限制
include范围:避免把测试、脚本等无关目录纳入主编译流程。
小结
TypeScript 编译慢并不是无解的难题,关键在于分清瓶颈所在:
- 如果是重复编译慢,优先开启增量编译。
- 如果是单体项目导致全量检查,考虑用项目引用拆分边界。
- 如果是转译本身成为瓶颈,用 SWC 把代码生成与类型检查解耦。
三者结合,可以让大型 TypeScript 项目在保持类型安全的同时,把编译时间从分钟级压到秒级。建议从增量编译入手,逐步引入项目引用,最后根据构建链路决定是否切换到 SWC,循序渐进地获得收益。
未经允许不得转载:任鹏个人博客 » 解决 TypeScript 编译速度慢的问题:项目引用、增量编译与 SWC 实践


朋友圈点赞图在线生成源码