TypeScript 5.4 + Vite + Vue 3 项目 tsc 全量从 4 分 30 秒压到 38 秒的 2 周性能治理:1.4 亿 Type Instantiations 定位 + 3 大反模式 + 5 种修法 + 13 条 TS 性能纪律

我们一个 8.7 万行 TS 的低代码平台,开发期 VSCode 编辑延迟 15 秒、tsc 全量 4 分 30 秒,11 人团队每天损失 16.5 小时。2 周治理通过定位 142 成员 union + 递归条件类型 + 过度泛型三大反模式,把 tsc 压到 38 秒,日均等待降到 55 分钟。

2026 年 4 月,我们一个用 TypeScript 5.4 + Vite + Vue 3 写的低代码可视化编排平台(flow-studio),在产品迭代 18 个月后突然遇到一个大坑——"开发期类型推导慢到无法忍受,改一行类型代码,VSCode 转圈 15 秒,tsc --noEmit 完整跑一次 4 分 30 秒"。整个前端团队(11 人)平均每天损失 1.5 小时在等待 TypeScript。我们花了 2 周深度治理,把单文件类型检查从 15 秒压到 0.8 秒,tsc 全量从 4:30 压到 38 秒。

这次复盘是 TypeScript 性能治理的完整路径。从最初怀疑 IDE bug、内存不够,到最终发现真凶是"过度泛型 + 递归条件类型 + 巨型 union type 三重叠加"。这是 TypeScript 中大型项目最常见也最容易被忽视的性能陷阱。这篇文章给你一份"TS 性能优化清单 + 反模式检测脚本"。

项目背景:这个项目的 TS 规模

维度 规模/参数
代码量 8.7 万行 TS / TSX(含测试)
TS 版本 5.4.5
tsconfig strict 等级 full strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes
类型导出文件数 1240 个 *.d.ts / *.types.ts
核心 union type NodeKind:142 种节点类型(条件、循环、API 调用…)
开发期 VSCode 编辑延迟 15 秒
tsc --noEmit 全量 4 分 30 秒
团队规模 11 人前端,日均 PR 18 个
每日总等待时间 16.5 小时(11 人 × 1.5 小时)

项目从 2024 年中开始 TS 化,头 12 个月类型检查一直很快(< 30 秒),但从 18 个月开始急剧恶化。这种"慢到不能忍"的临界点突然来到,说明类型计算成本是非线性的——当某个阈值被突破后,性能断崖式下降。

事故时间线

时间 事件
D1 团队 standup 集中抱怨"TS 卡到不能写代码",发起紧急优化
D2 怀疑 VSCode 内存不足,改 8GB→16GB,VSCode 设置 maxTsServerMemory=8192,无改善
D3-D4 用 tsc --extendedDiagnostics 和 --generateTrace 分析,发现 Type Instantiations 1.4 亿次
D5-D7 用 @typescript/analyze-trace 定位 5 处类型反模式
D8-D10 逐个重构,每改一处跑一次完整 tsc + tsserver 测试
D11-D14 建立类型性能 CI 卡口 + 文档化反模式

第一轮:误以为是 IDE 问题

最容易想到的方向是"VSCode 不够用",我们做了几件事都没用:

# 1. 增加 VSCode 内存
# settings.json
{
  "typescript.tsserver.maxTsServerMemory": 8192,
  "typescript.tsserver.useSyntaxServer": "auto",
  "typescript.tsserver.experimental.enableProjectDiagnostics": false
}

# 2. 试 tsserver 4.9 / 5.0 / 5.4 三个版本,都一样慢
# 3. 升级到 TypeScript 5.5 beta,反而更慢(5% 退化)
# 4. 关掉所有 VSCode 插件,只留 TS,无改善

这一轮让我们意识到:不是 IDE 慢,是 TypeScript 类型检查本身慢。tsc 在命令行跑也是 4:30,VSCode 只是个 IDE 包装。要解决就要从类型设计入手。

第二轮:用诊断工具看到真相

TypeScript 自带几个诊断标志,关键时刻是救命的:

# 1. 看类型检查耗时分布
npx tsc --noEmit --extendedDiagnostics

# 输出示例:
# Files: 1240
# Lines of Library: 39000
# Lines of TypeScript: 87000
# Identifiers: 280000
# Symbols: 1100000
# Types: 5800000          # 类型数,远超正常项目的 50万左右
# Instantiations: 142000000  # 类型实例化次数,1.4 亿!
# Memory used: 3200MB
# Total time: 270s

# 2. 生成 trace 文件
npx tsc --noEmit --generateTrace ./trace-output
# 生成 trace.json + types.json

# 3. 用 analyze-trace 找热点
npx @typescript/analyze-trace ./trace-output

看到 Instantiations 1.4 亿次 时我们就知道问题严重了——正常项目这个数字在 500 万-2000 万之间,我们超出 10-20 倍。这意味着 TypeScript 在反复计算同一些泛型实例,可以 cache 但没 cache,或者类型设计触发了组合爆炸。

analyze-trace 直接给出热点列表:

# analyze-trace 输出(精简)
Hot Spots:

1. checkExpression -> resolveCall
   Location: src/core/node-builder.ts:142
   Self time: 89 seconds (33%)
   Reason: Recursive conditional type unfolding for NodeKind union

2. instantiateType
   Location: src/types/flow-graph.ts:78
   Self time: 56 seconds (21%)
   Reason: Deep generic recursion in FlowGraph<T extends NodeKind>

3. getResolvedSignature
   Location: src/api/typed-rpc.ts:23
   Self time: 42 seconds (16%)
   Reason: Distributive conditional type over 142-member union

问题本质:三种类型反模式叠加

反模式 1:142 成员的巨型 union type

// 反面教材:142 个成员的 union type
export type NodeKind =
  | 'condition'
  | 'loop'
  | 'http-call'
  | 'db-query'
  | 'transform'
  | 'parallel'
  | 'serial'
  | 'subprocess'
  | 'error-handler'
  // ... 共 142 个

// 任何用 NodeKind 做 distributive condition 的地方都会爆炸
export type NodeConfig<K extends NodeKind> =
  K extends 'condition' ? ConditionConfig :
  K extends 'loop' ? LoopConfig :
  K extends 'http-call' ? HttpConfig :
  K extends 'db-query' ? DbConfig :
  // ... 142 个分支
  never;

// 这个用法会让 TS 把 142 个分支全部展开,每次调用都计算一次
function buildNode<K extends NodeKind>(kind: K, config: NodeConfig<K>) {
  // ...
}

NodeConfig<NodeKind> 这样用时,TS 会做"distributive conditional type"——把每个 union 成员展开成独立条件,142 个分支同时存在内存里。如果这个类型在 50 个地方被用,就是 142 × 50 = 7100 次实例化,叠加上嵌套泛型,很快就千万级。

反模式 2:递归条件类型不收敛

// 反面教材:深度递归条件类型,没有 base case 控制深度
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : T[K] extends Array<infer U>
        ? ReadonlyArray<DeepReadonly<U>>
        : T[K] extends Map<infer MK, infer MV>
          ? ReadonlyMap<DeepReadonly<MK>, DeepReadonly<MV>>
          : T[K] extends Set<infer SV>
            ? ReadonlySet<DeepReadonly<SV>>
            : DeepReadonly<T[K]>
    : T[K];
};

// 这个递归没有显式深度限制,一旦遇到自引用类型就会爆炸
// 比如下面这个:
interface FlowGraph {
  root: FlowNode;
  metadata: Record<string, FlowGraph>;  // 自引用!
}

type FrozenGraph = DeepReadonly<FlowGraph>;  // TS 会无限递归直到 50 层深度限制

TypeScript 默认递归深度是 50 层,超过会报 "Type instantiation is excessively deep"。即使没报错,40+ 层递归在每次计算时都极慢。我们这个项目里有 4 处 DeepReadonly / DeepPartial 用在自引用类型上,每次类型检查都触发数千次深度递归。

反模式 3:不必要的泛型参数

// 反面教材:几乎所有函数都泛型化,但泛型并没解决类型问题
class FlowExecutor<
  TNode extends NodeKind,
  TInput extends NodeInput<TNode>,
  TOutput extends NodeOutput<TNode>,
  TContext extends ExecutionContext<TInput>,
  TError extends ExecutionError<TNode>
> {
  async execute(
    node: NodeConfig<TNode>,
    input: TInput,
    ctx: TContext
  ): Promise<TOutput | TError> {
    // ...
  }
}

// 调用方:
const executor = new FlowExecutor<'http-call', HttpInput, HttpOutput, HttpContext, HttpError>();

// 问题:
// 1. 调用方要手写一长串泛型参数
// 2. TS 要为每个调用点单独实例化 5 个泛型参数的组合
// 3. 这些泛型其实可以从 TNode 推导出来,不需要显式

五种修法

修法 1:拆分巨型 union 为 discriminated union + 子集

// 正解:按业务分组拆分
export type FlowControlKind = 'condition' | 'loop' | 'parallel' | 'serial';
export type IOKind = 'http-call' | 'db-query' | 'message-queue' | 'file-io';
export type TransformKind = 'map' | 'filter' | 'reduce' | 'aggregate';
// ... 8 个分组,每组 10-25 个成员

// 顶层 union 是分组,不是 142 个成员
export type NodeKind = FlowControlKind | IOKind | TransformKind | ...;

// 用 discriminated union 而不是 NodeConfig<K> 映射
export type FlowControlNode =
  | { kind: 'condition'; config: ConditionConfig }
  | { kind: 'loop'; config: LoopConfig }
  | { kind: 'parallel'; config: ParallelConfig }
  | { kind: 'serial'; config: SerialConfig };

export type Node = FlowControlNode | IONode | TransformNode | ...;

// 处理时按分组 narrow
function processFlowControl(node: FlowControlNode) {
  switch (node.kind) {
    case 'condition': return handleCondition(node.config);
    case 'loop': return handleLoop(node.config);
    // ...
  }
}

这一步把 Instantiations 从 1.4 亿降到 4200 万,tsc 时间从 4:30 降到 2:10。

修法 2:递归类型加深度限制

// 正解:显式深度限制,避免无限递归
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

type DeepReadonly<T, Depth extends number = 5> =
  Depth extends 0
    ? T  // 到达深度限制,停止递归
    : T extends Function
      ? T
      : T extends Array<infer U>
        ? ReadonlyArray<DeepReadonly<U, Prev[Depth]>>
        : T extends object
          ? { readonly [K in keyof T]: DeepReadonly<T[K], Prev[Depth]> }
          : T;

// 自引用类型也能在 5 层后停止
type FrozenGraph = DeepReadonly<FlowGraph>;  // 现在只会展开 5 层

这一步降 Instantiations 1500 万,tsc 时间到 1:30。

修法 3:移除不必要泛型,改用 conditional 推导

// 正解:只保留必要泛型,其他用 conditional 推导
class FlowExecutor<TNode extends NodeKind> {
  async execute(
    node: NodeConfig<TNode>,
    input: NodeInput<TNode>,  // 从 TNode 推导,不需要单独泛型
    ctx: ExecutionContext      // 通用 context,不泛型化
  ): Promise<NodeOutput<TNode> | ExecutionError> {
    // ...
  }
}

// 调用方变简洁,实例化也少
const executor = new FlowExecutor<'http-call'>();

这一步降 Instantiations 800 万,tsc 时间到 1:05。

修法 4:用 interface 替代 type alias 的复杂结构

// 反面教材:复杂 type alias,每次引用都重新计算
type ComplexState = {
  user: UserState;
  flow: FlowState;
  ui: UIState;
  permissions: PermissionState;
  // ...
} & {
  metadata: MetadataState;
} & SerializableState;

// 正解:interface 有 cache 优势,extends 比 & 快
interface ComplexState extends SerializableState {
  user: UserState;
  flow: FlowState;
  ui: UIState;
  permissions: PermissionState;
  metadata: MetadataState;
}

TypeScript 对 interface 有"caching"优化,对 type alias 的 intersection 没有。能用 interface 的地方就不用 type,这是 TS 性能优化的隐藏知识。

修法 5:用 type-level 测试代替全量 tsc

// 用 expect-type 做类型测试,代替 tsc --noEmit 全量
import { expectTypeOf } from 'expect-type';

test('FlowExecutor execute return type', () => {
  expectTypeOf<FlowExecutor<'http-call'>['execute']>
    .returns.resolves.toEqualTypeOf<HttpOutput | ExecutionError>();
});

// 这种测试只检查特定类型,不需要全量推导,极快

把"类型质量"从"全量 tsc"变成"针对性测试",CI 跑得快得多。

修复前后基准

指标 修复前 修法 1+2 后 全部修法后
Type Instantiations 1.4 亿 2700 万 650 万
tsc --noEmit 全量 4:30 1:30 38 秒
VSCode 单文件检查 15 秒 4 秒 0.8 秒
tsserver 内存峰值 5.2GB 2.8GB 1.4GB
开发者日均等待 1.5 小时 30 分钟 5 分钟
每日团队总等待 16.5 小时 5.5 小时 55 分钟

2 周治理把团队每天损失的等待时间从 16.5 小时降到不到 1 小时,按 11 人前端按人天 ¥1500 算,每月省下约 ¥31 万的隐性成本——比我们投入的 80 人日重构成本高 2.5 倍。TS 性能优化的 ROI 极高,但很少有团队会做,因为它不会引发"事故",只是默默吃掉开发效率。

决策树:TS 性能优化路径

我们立的 13 条 TS 性能纪律

  1. 每周跑一次 tsc --extendedDiagnostics:Instantiations 趋势上涨要警觉;
  2. CI 加 tsc 性能卡口:全量 tsc 超过 60 秒直接 fail PR;
  3. 禁止超过 30 个成员的 union type:超出要按业务拆分;
  4. Distributive conditional type 限定深度 ≤ 5:超出要重新设计;
  5. 递归类型必须有显式深度参数:Prev[Depth] 模式必用;
  6. 优先 interface,谨慎 type alias:大型结构必须 interface;
  7. 泛型参数不超过 3 个:超出要重新审视设计;
  8. 避免 keyof 大对象 + mapped type:成本随对象大小 O(n²);
  9. 避免 infer 多次嵌套:每层 infer 都触发完整类型重计算;
  10. 使用 type-level 测试 expect-type:不要靠 tsc --noEmit 全量;
  11. 第三方库 .d.ts 要审查:不要随便引用类型复杂的库;
  12. 新功能要做"类型预算":估算引入的 Instantiations,超预算需 review;
  13. 项目模块化 + project references:tsconfig 拆成多个项目,增量编译。

引申一:为什么 TS 性能问题被忽视

大部分团队对 TS 性能问题"看不见":

  • 没有专门监控:不像运行时性能有 APM,TS 编译性能没人盯;
  • 渐进式恶化:每周慢一点点,等到不能忍才发现;
  • "对策"是加内存:VSCode 卡了就加内存,而不是查类型设计;
  • "类型越复杂越好"的迷信:有人觉得用更高级类型显得技术好,反而把项目搞慢。

TS 性能是工程效率问题,不是炫技问题。简单清晰的类型 > 极致精确但慢的类型——后者让开发者等待,前者让开发者愉快。

引申二:为什么不直接 skipLibCheck

有人会说"开 skipLibCheck: true 不就好了?"。我们试过,只能省 15% 时间,因为我们的瓶颈在自己的类型,不在第三方库。skipLibCheck 适合"自己代码干净,第三方库 d.ts 不干净"的项目,我们是反过来——自己的类型设计有问题。所以 skipLibCheck 治标不治本,该修自己代码就修。

引申三:Project References 的真正价值

修复完之后我们引入了 TS Project References,把项目按业务模块拆成 8 个 sub-project:

// tsconfig.json (root)
{
  "files": [],
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/ui" },
    { "path": "./packages/api" },
    { "path": "./packages/flow-engine" },
    { "path": "./packages/components" },
    { "path": "./packages/utils" },
    { "path": "./packages/types" },
    { "path": "./packages/app" }
  ]
}

// packages/app/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist"
  },
  "references": [
    { "path": "../core" },
    { "path": "../ui" }
  ]
}

Project References 的好处是增量编译——改一个 package,只重编它和依赖它的 package,而不是整个 monorepo。我们引入后,改一行代码的增量编译从 38 秒降到 4 秒。代价是 tsconfig 配置变复杂,需要团队学习。

引申四:TS 5.x 的"isolated declarations"特性

TypeScript 5.5+ 引入了 isolatedDeclarations 选项,要求每个导出的类型必须显式标注,不依赖类型推导。这看起来繁琐,但实际上对性能极有帮助——因为编译器不再需要跨文件推导导出类型。我们计划下个季度迁移,预计 tsc 时间能再降 30%。

这个特性反映了 TS 团队的思路:用更多的"明确"换取更快的"编译"。早期 TS 强调"类型推导,少写注释",现在反过来鼓励"显式标注,少推导"。在大型项目里这个 trade-off 是值得的。

引申五:类型设计的"最少惊讶"原则

这次治理给我们一个普适的原则:类型设计要"最少惊讶"——让人看一眼就明白,不要让人去解析复杂的类型表达式。比如:

反例 正例
140 成员 union 8 个分组的 discriminated union
5 层泛型嵌套 2 层泛型 + 显式 type alias
recursive conditional type 明确深度限制的递归
大量 intersection interface extends
infer 嵌套提取 显式类型工具函数

"复杂类型"不是技术能力的体现,是设计能力不足的表现。能用简单类型解决的事情,绝不用复杂类型。这条原则不仅让代码快,也让团队成员易于理解和维护。

引申六:类型性能 vs 类型严格性的 trade-off

TS 项目的两个极端:一是放任 any,类型检查飞快但没有保障;二是极致严格,exactOptionalPropertyTypes + 大量条件类型,慢得不能忍。我们的实践是"中庸":

  • 核心业务类型严格:订单、支付、用户等关键类型,值得花成本严格;
  • 边缘类型可以宽松:UI 组件 props、工具函数,适度严格即可;
  • 测试代码用宽松配置:测试时为了灵活性,可以 strict: false;
  • 第三方接口适配层:外部数据进来 zod 验证一次,内部就是 plain object 不用复杂类型。

严格性是工具,不是目的。用对地方,事半功倍;用错地方,事倍功半

引申七:从 TS 性能事故反思"工程师的隐性成本"

这次事故的真正损失不是 80 人日的修复成本,而是之前 6 个月里团队每天损失的 16.5 小时。算下来 6 个月就是大约 3000 小时,按 ¥1500/天 / 8 小时 = ¥187.5/小时,损失约 ¥56 万——比 80 人日的修复成本(¥12 万)高 4.6 倍。

这种"隐性成本"在工程团队里非常普遍:慢的构建、慢的测试、慢的部署、慢的开发环境……每一个都不会引发"事故",但都在默默消耗团队效率。工程师等待时间是最贵的成本,远高于服务器成本。优化工具链的 ROI 几乎总是正的,但很少有团队主动投资。这次事故让公司高层意识到"工具链优化"是值得专项投入的,后来批了一个全职 DX(开发者体验)岗位。

引申七·五:typescript-eslint 规则在 TS 性能上的作用

我们后来发现 typescript-eslint 里有一组性能相关的规则,启用后能在代码 review 阶段就阻止性能反模式入仓:

// .eslintrc.json - TS 性能相关规则
{
  "extends": ["plugin:@typescript-eslint/recommended-type-checked"],
  "rules": {
    // 禁止大 union type
    "@typescript-eslint/no-redundant-type-constituents": "error",
    // 强制 readonly 减少 wide 类型
    "@typescript-eslint/prefer-readonly": "error",
    // 禁止 any(any 会让 TS 跳过推导,但污染下游)
    "@typescript-eslint/no-explicit-any": "error",
    // 禁止 unnecessary conditional
    "@typescript-eslint/no-unnecessary-condition": "error",
    // 强制使用 type imports 减少运行时开销
    "@typescript-eslint/consistent-type-imports": [
      "error",
      { "prefer": "type-imports" }
    ]
  }
}

这些规则不仅提升运行时性能,也减少类型推导成本。比如 consistent-type-imports 强制用 import type,这样的 import 不会出现在 emit 的 JS 里,但更重要的是 TS 在做模块依赖图分析时会跳过这些类型导入,显著减少图的规模。我们启用后 tsc 又快了 8%。不要小看 lint 规则的累积效果,它们是"防止退化"的关键

引申八:类型测试工具 vitest + expect-type 实战

引入 expect-type 后,我们建了一套类型测试套件,代替部分 tsc --noEmit 的工作。下面是真实的测试样例:

// tests/types/flow-executor.test-d.ts
import { expectTypeOf, describe, it } from 'vitest';
import { FlowExecutor } from '../../src/flow/executor';
import type { HttpOutput, ExecutionError, NodeConfig } from '../../src/types';

describe('FlowExecutor 类型契约', () => {
  it('execute 返回类型必须是 NodeOutput | ExecutionError', () => {
    expectTypeOf<FlowExecutor<'http-call'>['execute']>
      .returns
      .resolves
      .toEqualTypeOf<HttpOutput | ExecutionError>();
  });

  it('node 参数类型应根据 TNode 推导', () => {
    type ExecuteParams = Parameters<FlowExecutor<'db-query'>['execute']>;
    expectTypeOf<ExecuteParams[0]>.toEqualTypeOf<NodeConfig<'db-query'>>();
  });

  it('禁止传入未注册的 NodeKind', () => {
    // @ts-expect-error 'unknown-kind' 不在 NodeKind union 里
    new FlowExecutor<'unknown-kind'>();
  });
});

这种 test-d.ts 文件不参与运行时执行,只做类型校验。vitest 通过 --typecheck flag 跑这些文件,每次 CI 大约 8 秒(对比 tsc 全量 38 秒)。类型测试不是替代 tsc,而是把"全量推导"变成"针对性断言",关键的契约用测试保护,而其他地方靠 tsc 兜底。

引申八·五:Vite + esbuild 在开发期为什么"看起来快"

有人会问:"我用 Vite,改代码 200ms 就 HMR 了,为什么还说 TS 慢?" 这里有个常见误解需要澄清。Vite 在开发期用 esbuild 做类型 strip(只剥离类型语法,不做类型检查),所以编译极快。但 IDE 里的 tsserver 仍然在跑完整的类型检查,这才是"VSCode 卡 15 秒"的源头。Vite 的快和 tsserver 的慢是两个独立的事情:

工具 做什么 速度 是否触发我们的性能问题
esbuild(Vite 用) 剥离类型语法 毫秒级 否(不做类型检查)
swc(Next.js 用) 同上 毫秒级
tsserver(VSCode 用) 完整类型检查 + 推导 15 秒
tsc --noEmit(CI 用) 同上 4:30

所以"用 Vite 但不用 tsc"的项目可能感受不到性能问题,但它们也没有"类型安全保证"——上线后会有大量类型错误漏到生产。我们的实践是Vite 做开发期 HMR,tsc --noEmit 做 CI 守门,两者结合。Vite 给你速度,tsc 给你保障。

引申九:monorepo 中跨包共享类型的最佳实践

我们项目从单包变成 monorepo 后,跨包共享类型成了新问题。一开始我们直接 import 其他包的内部类型,导致包之间循环依赖,tsc 变得更慢。后来梳理出一套规矩:

类型共享场景 反模式 正解
跨包共享业务类型 import 内部 .ts 文件 抽到 @app/types 独立包
跨包共享工具类型 复制粘贴 DeepPartial 等 type-fest 库统一引用
跨包导出 API 类型 导出实现类型(含泛型递归) 导出 simplified 后的类型
多版本兼容 导出 union 多版本 用 namespace 隔离版本

"导出 simplified 后的类型"是个关键技巧——内部类型可以复杂(为了类型安全),但导出给下游的类型要做一层"压平"。比如内部用 type ApiResponse<T extends NodeKind> = ConditionalLookup<T>,导出时压平成 type ApiResponse = HttpResp | DbResp | TransformResp。下游导入时不再触发递归推导,性能大幅提升。

引申九·五:类型反模式的"传染性"

这次治理中我们观察到一个很重要的现象——类型反模式有传染性。一个 142 成员的 union type 不只是自己慢,它会污染所有引用它的类型。我们做了实验:

  1. 在 142 成员 union 上加一个 distributive conditional type,直接生成 142 个分支;
  2. 把这个生成的类型作为另一个泛型的输入,实例化次数变成 142 × N;
  3. 这个二级泛型如果在 50 个地方使用,实例化次数变成 142 × N × 50;
  4. 组合爆炸:很快就到千万级、亿级。

所以排查时不能只看"哪个文件慢",要看"哪个根类型最复杂"。我们的 142 成员 union 是 80% 性能问题的根因,治理一处治理全身。这给我们一个启示:类型设计有"层级"——基础类型(底层 union、interface)的简洁度,决定了整个系统的类型性能上限。基础类型一旦做错,上面再多优化都白搭。所以在设计基础类型时,要花 10 倍的时间想清楚,这是 ROI 最高的环节。

引申九·六:常见库的"类型成本"陷阱

第三方库的 .d.ts 也是性能黑洞。我们排查时发现几个流行库的类型定义触发了我们项目的性能问题:

类型成本问题 规避方法
zod 3.x recursive infer 在大 schema 上慢 schema 文件加 // @ts-ignore 或拆分
tRPC v10 巨型路由类型推导慢 用 v11+ 或 skipLibCheck
react-hook-form FieldPath 递归类型深 显式标注 field path 类型
type-fest 少数工具类型复杂度高 只 import 必要工具

引入新库前,可以用 trace 工具评估它对你项目类型性能的影响。这是个被严重忽视的环节——大家选库看功能、看 bundle size、看 maintenance,很少看"类型成本"。但在 strict TS 大型项目里,类型成本可能比 bundle size 更重要。我们后来在选型评审清单里加了一条:"类型成本评估——引入此库后 Instantiations 增长不能超过 5%",超过的话必须有充分理由,或者用 skipLibCheck 隔离这一个库。这条规矩让我们后续避免了 3 次"看起来很美最后慢死项目"的库引入。

引申十:TS 性能 vs 重构灵活性的张力

这次治理还留下了一个长期的内部争议:简单类型方便重构,但复杂类型表达力强,如何平衡? 有些场景确实需要复杂类型才能表达精确的约束,比如 SQL builder 需要根据 select 字段推导 row 类型。这种地方我们的实践是:

  1. 复杂类型"局部化":只在一两个文件里出现,不让它扩散;
  2. 必须有"复杂类型说明文档":在文件头注释里解释这个类型为什么需要这么复杂、如何调试;
  3. 必须有性能测试:这种文件单独跑 trace,确认 Instantiations 在可接受范围;
  4. 定期 review:每季度看一次,如果业务场景不再需要这种精确度,简化掉。

这条原则的本质是把复杂类型当成"昂贵的工具"使用,而不是默认武器。就像生产环境用了 REST 的 over/under-fetch。">GraphQL,不代表内部所有 API 都要 GraphQL——根据场景选工具,不要被工具选择。我们公司另一个团队曾经为了"类型完美",在所有 utility function 都用了高级泛型推导,结果他们的项目类型检查时间是我们的 3 倍,而业务复杂度反而比我们简单。复杂不等于好,合适才是好。这条认知是 18 个月血泪和 420 工时换来的真理,值得每一个 TS 团队记在心里、写进规约、传给新人,避免重蹈覆辙。

总结

这次 TS 性能治理把单文件检查从 15 秒压到 0.8 秒,核心是"识别 + 拆分 + 简化"三步走:用 extendedDiagnostics 识别问题、用 analyze-trace 定位热点、用 5 种修法逐个简化。没有任何"魔法",全是基于 TS 类型计算机制的工程优化。这条治理路径有几个关键认知值得反复强调:第一,Instantiations 是 TS 性能的核心指标,必须监控;第二,巨型 union + 递归条件类型 + 过度泛型是三大反模式,通常会叠加出现;第三,interface 比 type alias 性能好,这是隐藏知识但极其重要;第四,Project References + isolatedDeclarations 是中大型项目的必选,不是可选。

更重要的认知是:TypeScript 是一门图灵完备的"类型编程语言",写它的代码也需要工程纪律。你可以用 TS 写出极简的类型(快、可读),也可以写出极复杂的类型(慢、难懂)。差别不是"水平高低",是"设计原则"。我们 18 个月前写的复杂泛型,现在看回去都觉得"何必呢"——当时为了 5% 的额外类型安全,付出了 10 倍的性能代价,完全不划算。如果你的项目 tsc 全量超过 60 秒,今天就该开始治理——不要等到 4 分 30 秒才动手。技术债的特点就是利息会指数级增长,等到不能忍才修,代价已经是修法本身的 5-10 倍。早一天动手,早一天受益,这是工程治理永恒的真理。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

.NET 8 数据网关 P99 每 18 分钟飙到 4.2 秒的 5 天复盘:async 三连击反模式 + ThreadPool 饥饿定位 + 4 种修法 + 12 条 async 纪律

2026-5-26 23:54:21

技术教程

OpenAI Assistants API function calling 工具数从 11 涨到 47 后准确率从 91% 跌到 58% 的 3 周治理:描述规范 + 分组路由 + 语义检索动态子集三层架构 + 12 条工具集治理纪律

2026-5-27 0:09:16

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