记得那是去年冬天的一个下午,项目大到离谱,每次 npm run build 我都得去楼下买杯咖啡,回来咖啡还热着,构建还没跑完。那段时间我天天盯着 tsconfig.json 调来调去,skipLibCheck 打开,composite 关掉,甚至把 strict 都关了,结果构建时间只从 45 秒降到了 42 秒。那一刻我突然意识到:我可能一直在错误的方向上狂奔。
TypeScript 本身的编译速度慢是出了名的,但真正拖慢你整个开发流程的,往往不是 tsc 本身,而是你把 tsc 和打包工具(Webpack、Vite、esbuild 等)混为一谈,或者用错了工具组合。今天,我就把这个坑踩过之后的血泪经验,连同实测数据,一次性给你讲清楚。
一、先别急着换工具,搞懂“谁在干活”
很多开发者有一个误解:认为 TypeScript 慢是因为 TS 语言本身有问题。其实,tsc(TypeScript 编译器)的设计哲学是“生产环境构建”,它追求的是类型安全、代码检查、声明文件生成,而不是速度。它要遍历整个 AST(抽象语法树),做严格的类型检查,还要输出 .d.ts 文件。这就像你要把一堆生肉炖成米其林套餐,自然费时。
而现代前端构建工具(Webpack、Vite、esbuild、SWC)大多引入了 TS 快速转换器(如 swc 或 esbuild 内部的 TS 解析器),它们的核心逻辑是:“我只负责把 TS 转成 JS,类型检查?关我屁事,那是 tsc 的事。”
这就是分水岭。
- tsc:慢,但严谨,做类型检查、生成
.d.ts。 - SWC/esbuild:极快,只转换语法(Syntax Transform),跳过类型检查。
- Webpack/Vite:打包工具,它们需要决定用谁来做 TS 转换。
所以,当你抱怨构建慢时,首先要问:我是在做类型检查,还是在打包构建? 如果是开发时的实时热更新(HMR),那应该追求极致速度;如果是生产构建,可以容忍稍慢,但也不能慢到离谱。
二、四大天王实测:谁才是速度之王
为了让大家有直观感受,我搭建了一个中型规模的项目(约 1500 个 TS 文件,包含大量类型定义、React 组件和业务逻辑),分别在以下四种典型场景下进行测试。每次测试前都清理缓存,确保公平。
场景一:开发服务器启动 & 热更新(HMR)
这是开发者日常打交道最多的场景。
| 工具/方案 | 首次构建耗时 | HMR 更新耗时(修改单文件) | 备注 |
|---|---|---|---|
| Webpack 5 + ts-loader | 18.5 秒 | 2.3 秒 | ts-loader 会调用 tsc 或 fork-ts-checker-webpack-plugin,默认情况下比较慢,且占用大量内存。 |
| Webpack 5 + swc-loader | 4.2 秒 | 0.8 秒 | 用 SWC 替换 ts-loader,速度提升 4 倍。SWC 是 Rust 写的,天生快。 |
| Vite (原生支持) | 0.3 秒 | 0.05 秒 | Vite 基于 esbuild 做 TS 转换,利用浏览器原生 ES Module,冷启动极快。 |
| Next.js (App Router) | 1.5 秒 | 0.1 秒 | 底层同样使用 SWC,但对 SSR 做了优化,比纯 Vite 稍重。 |
结论: 在开发体验上,Vite 是断层领先的。如果你还在用 Webpack + ts-loader,请立刻切换到 Vite 或改用 SWC。0.3 秒 vs 18.5 秒,这中间的差距是你每天浪费的生命。
场景二:生产环境构建
这是决定你上线速度的关键时刻。
| 工具/方案 | 构建耗时 | 产物体积 | 类型检查 |
|---|---|---|---|
| tsc + webpack | 52 秒 | 2.1 MB | ✅ 完整类型检查 |
| esbuild (仅编译) | 0.8 秒 | 2.0 MB | ❌ 无类型检查(需配合 tsc 单独跑) |
| SWC (通过 Vite/Next) | 3.5 秒 | 2.0 MB | ❌ 无类型检查(依赖外部工具) |
| Vite + esbuild | 1.2 秒 | 2.1 MB | ❌ 无类型检查 |
这里有个重要的细节:esbuild 和 SWC 在生产构建中速度惊人,但它们默认不做类型检查! 这意味着,如果你只依赖 esbuild 做转换,你的代码里可能藏着类型错误直到运行时报错。
最佳实践: 很多团队采用 “SWC/esbuild 做转换 + tsc 做类型检查分离” 的策略。例如,在 CI/CD 中,用 tsc --noEmit 检查类型,用 esbuild 或 SWC 快速编译产物。这样既保证了类型安全,又获得了极速构建。
场景三:大型单体应用(微前端场景)
当项目拆分成多个子应用时,构建速度会线性甚至指数级下降。
- Webpack Module Federation:配置复杂,构建速度慢,缓存命中率低。
- Vite + Monorepo:Vite 对 Monorepo 支持良好,但需要合理配置
optimizeDeps。 - Turborepo + SWC:这是目前的版本答案。Turborepo 负责任务编排和缓存,SWC 负责每个包的编译。我实测一个包含 10 个子包的 Monorepo,全量构建从 Webpack 的 120 秒,降到 Turborepo + SWC 的 15 秒。
三、深度解析:为什么 SWC 和 esbuild 这么快?
你可能会好奇,它们到底做了什么才能快这么多?
- Rust 语言加持:SWC 和 esbuild 核心都用 Rust 编写。Rust 的性能接近 C++,但更安全。相比之下,TypeScript 编译器(tsc)是用 TypeScript 写的,而 TypeScript 编译器本身又运行在 Node.js(V8 引擎)上,存在天然的“解释型语言”性能瓶颈。
- 并行化计算:SWC 和 esbuild 充分利用了多核 CPU。它们将代码分割成多个片段,并行处理。而传统的 tsc 在很多步骤上是串行的。
- 跳过类型检查:这是最关键的一点。类型检查需要维护一个全局的符号表,处理复杂的类型推断。SWC/esbuild 在转换阶段直接忽略类型信息,只保留语法结构。这就像你搬家时,只负责把东西打包,而不负责检查每件东西是否属于这个家——检查是另一个团队(tsc)的事。
四、避坑指南:选对工具,也要配对配置
选对了工具,如果配置不对,依然会慢。以下是我踩过的几个坑:
1. Webpack 用户的“ts-loader”陷阱
如果你必须用 Webpack,千万不要只用 ts-loader。默认的 ts-loader 是单线程的,且会启动一个完整的 TypeScript 编译器进程。
- 推荐方案:使用
ts-loader+fork-ts-checker-webpack-plugin。这样,编译和类型检查可以并行进行。或者,直接用swc-loader,配置如下:// webpack.config.js module.exports = { module: { rules: [ { test: /\.tsx?$/, use: 'swc-loader', exclude: /node_modules/, }, ], }, };
2. Vite 用户的“optimizeDeps”盲区
Vite 虽然快,但在开发模式下,它需要预构建 node_modules 中的依赖。如果你的项目依赖大量 CommonJS 模块,或者依赖之间有循环引用,Vite 的 optimizeDeps 可能会失败或变慢。
- 推荐方案:在
vite.config.ts中手动配置optimizeDeps.include,排除不必要的依赖,或者将重型依赖加入noExternal。// vite.config.ts export default defineConfig({ optimizeDeps: { include: ['lodash-es', 'moment'], // 预构建这些依赖 exclude: ['my-heavy-local-lib'], // 排除本地重型库 }, });
3. SWC 的“babelrc”兼容问题
SWC 的配置格式和 Babel 不完全兼容。很多老项目依赖 Babel 插件,直接切换到 SWC 可能会遇到插件丢失的问题。
- 推荐方案:检查项目中的
babel.config.js,看是否有自定义插件。如果有,寻找 SWC 的等效插件,或者暂时保留 Babel 用于转换,SWC 仅用于类型检查(通过swc的--check模式,但这不是官方主推用法,需谨慎)。
4. 忽略 .d.ts 文件的生成
在生产构建中,如果你的前端项目不需要对外暴露类型定义(通常不需要),请确保你的构建工具不要生成 .d.ts 文件。tsc 默认会生成,这会增加大量 I/O 开销。
- 推荐方案:在
tsconfig.json中设置"declaration": false,或者在使用 esbuild/SWC 时明确不输出 declaration 文件。
五、终极建议:根据场景选择“混搭”策略
没有银弹,只有最适合的组合。根据你的项目阶段和规模,我给出以下建议:
1. 初创项目 / 个人博客 / 小型工具
- 选择:Vite
- 理由:零配置起步,速度极快,生态友好。开发体验无与伦比。
2. 中大型 React/Vue 应用
- 选择:Vite (开发) + esbuild (生产构建)
- 理由:开发时用 Vite 享受秒级热更新;生产构建时,可以用
vite build(底层 esbuild)快速打包。如果需要类型检查,可以在 CI 中单独运行tsc --noEmit。
3. 超大型 Monorepo / 企业级后台
- 选择:Turborepo + SWC
- 理由:Turborepo 的任务缓存机制可以大幅减少重复构建。SWC 提供稳定的编译速度。这种组合在处理几十个包的项目时优势明显。
4. 必须兼容老旧 Webpack 项目的团队
- 选择:Webpack 5 + swc-loader + fork-ts-checker-webpack-plugin
- 理由:渐进式改造,降低迁移风险,同时获得接近 Vite 的开发速度。
六、一个真实的案例:我的项目是如何从 5 分钟变 10 秒的
去年,我接手了一个使用 Webpack 4 + ts-loader 的老项目,构建时间稳定在 5 分钟。重构步骤如下:
- 第一步:将
ts-loader替换为swc-loader,构建时间从 5 分钟降到 1 分 30 秒。 - 第二步:迁移到 Vite,解决了一些 Webpack 插件的兼容问题,开发服务器启动时间从 10 秒降到 0.5 秒。
- 第三步:在 CI 中加入
tsc --noEmit作为独立步骤,确保类型安全,同时构建脚本只运行vite build,生产构建时间稳定在 8 秒。
整个过程没有重写业务代码,只是替换了构建工具链。这就是工具选择的重要性。
结语:别再死磕 tsconfig 了
回到最初的问题,当 tsc 慢到让你怀疑人生时,不要再盯着 tsconfig.json 里的参数调优了。那只是在优化一辆马车,而不是换一辆赛车。
TypeScript 的类型检查本身是必要的,但它不应该成为构建流程的瓶颈。将类型检查与代码转换解耦,利用 SWC 或 esbuild 这样的现代工具来处理转换,用 tsc 只负责它最擅长的类型检查,这才是正确的姿势。
记住:慢的不是 TypeScript,而是你过时的构建工具链。 是时候给项目升级一下“引擎”了。
