想象你是一家高档餐厅的主厨,要为一场百人宴会准备 20 道菜。传统做法是:哪怕只有一位客人临时加了一道小菜,你也要把冷菜、热菜、汤品、甜点全部从零开始重做一遍——洗菜、切配、起锅、摆盘,忙到深夜。这就是 TypeScript 编译器的“全量编译”:每次改动一行代码,它都重新读取、解析、类型检查、生成全部文件。 聪明的你换了一种方式:把每道菜的做法记在一本厚厚的工作笔记里——哪道菜已经完成、用了哪些食材、哪道菜依赖哪道菜。客人加菜时,你翻开笔记,发现只需要重做那一道新菜,外加两道用了同一种酱汁的旧菜,其余 17 道直接端上来就行。这就是 TypeScript 的“增量编译”:借助 .tsbuildinfo 缓存文件记录编译状态,只重新处理“变了的文件”和“受波及的文件”。 本文将从这本“工作笔记”出发,带你理解编译为什么慢、增量编译如何加速、如何用项目引用分灶管理、如何配合 watch 模式与构建工具,最后给出实战优化清单。

岗位关联

对 AI 从业者而言,掌握编译性能优化是驾驭 AI 辅助编程与大规模前端工程的核心基本功:AI 每秒钟都在产生新代码,编译越快,你的迭代飞轮就转得越快。

目录
  1. 可视化演示
  2. 编译性能瓶颈
  3. 增量编译原理
  4. 项目引用拆分
  5. Watch模式
  6. 构建工具集成
  7. 优化策略
  8. 实践与排查
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示TS全量编译流程、增量编译的文件变更检测与缓存复用机制、以及全量与增量编译的性能对比,交互切换三种模式理解编译优化原理。

本节配有交互动效(进入视口后自动加载,约 3KB)。

'+captions.full+'

编译性能瓶颈

宴会为什么慢?不是大厨手慢,而是“每道菜都重做”这件事本身。假设宴席上有 100 道菜,每道菜平均要 5 分钟,一次全量备菜就是 500 分钟。TypeScript 编译器也一样:一个 10 万行代码的工程,全量编译动辄几十秒甚至数分钟。如果你改一个字就要等这么久,开发节奏会被彻底拖垮——这就像客人催菜时,你还在从头熬高汤。

编译器的工作分三步:解析(把 .ts 源码拆成语法树)、类型检查(沿着 import 引用关系逐文件核对类型)、产物生成(输出 .js 与声明文件)。全量编译时,这三步对每一个文件都要完整执行,而且类型检查是跨文件的:一个文件里定义了接口,一百个文件引用了它,改动这个接口,那一百个文件都要重新核对一遍。因此耗时随文件数量线性增长,而类型检查的波及面还会让实际耗时远超想象。

所以问题一目了然:全量编译相当于“每次加菜都重做整桌”。要提速,就得像聪明的厨师那样——不做无用功。接下来我们看看那本“工作笔记”是怎么改变一切的。

Ttotal=Tparse+Tcheck+Temit≈N×tf\displaystyle T_{\text{total}} = T_{\text{parse}} + T_{\text{check}} + T_{\text{emit}} \approx N \times t_f
总耗时约等于文件数乘单文件平均耗时:文件越多、单文件越重,全量编译越慢。
  1. T_parse 解析阶段编译器读取源码文件并生成语法树,相当于大厨把菜谱逐字读懂并记在脑中。
    TparseT_{\text{parse}}
  2. T_check 检查阶段跨文件核对类型是否自洽,相当于核对每道菜的食材是否与其他菜冲突。
    TcheckT_{\text{check}}
  3. T_emit 产物阶段把 TS 转成可运行的 JS 与声明文件,相当于把半成品装盘出锅。
    TemitT_{\text{emit}}
  4. N × t_f 线性估算N 是文件总数,t_f 是单文件平均耗时;文件越多,总耗时越高。
    N×tfN \times t_f

增量编译原理

大厨的工作笔记,就是 TypeScript 的 .tsbuildinfo 文件。它记录了三样东西:每个文件的“版本指纹”(内容哈希)、文件之间的依赖关系、以及哪些文件已经完成。有了它,编译器就像有了“记性”——不再是一个每次开工都失忆的临时工。

开启 incremental 编译选项后,tsc 每次编译会先读取 .tsbuildinfo。编译时,它先对比所有文件的哈希,找出真正变更的文件;再沿着依赖图,找出“下游受影响”的文件——比如你在 a.ts 里改了一个接口,import 了 a.ts 的 b.ts、c.ts 都要重新做类型检查。只有这部分文件会被重新处理,其余文件直接从缓存标记为已是最新,连打开都不需要。

记事本的威力在于“知道什么不用做”。不过,当宴会规模大到几十个团队同时备菜时,一本笔记本也不够——我们需要把厨房分成若干个独立灶台,这就是项目引用。

Tincr=tc+∑d∈D(c)td,Tincr≪Tfull\displaystyle T_{\text{incr}} = t_c + \sum_{d \in D(c)} t_d, \qquad T_{\text{incr}} \ll T_{\text{full}}
增量编译耗时 = 变更文件耗时 + 所有下游依赖文件耗时之和,通常远小于全量耗时。
  1. t_c 变更文件开销重新解析并检查真正被修改的那个文件,相当于一道新菜的制作成本。
    tct_c
  2. 求和运算符把下游所有受波及文件的开销逐项累加,相当于把同酱汁的几道旧菜一起重做。
    ∑d∈D(c)\sum_{d \in D(c)}
  3. D(c) 下游集合依赖变更文件 c 的全部文件集合,由依赖图推导出来。
    D(c)D(c)
  4. 远小于关系当一次改动只影响少量文件时,增量耗时接近常数,与工程总量几乎无关。
    Tincr≪TfullT_{\text{incr}} \ll T_{\text{full}}

项目引用拆分

一家大餐厅有三层楼:一楼冷菜、二楼热菜、三楼甜点。如果所有菜都挤在一个灶台做,光找锅铲就要半天。Project References 的思路就是把工程拆成多个子项目,每个子项目有自己的菜谱(tsconfig)和成品(编译产物),就像分灶管理,各灶互不干扰。

根 tsconfig.json 通过 references 字段列出子项目;每个子项目必须开启 composite: true(它自动开启 incremental 并强制生成 .d.ts 声明文件)。构建时,被依赖的项目先构建,产出声明文件;依赖方只需读取这些声明文件,而不用重新解析源码。构建顺序由依赖图决定——这就是拓扑排序:先做冷菜,再做热菜,最后拼盘,次序不能乱。

分灶之后,热菜师傅再也不用等冷菜师傅的全部步骤,只需要验收对方的半成品。但还有个问题:灶台要烧起来,得有人盯着食材柜——这就是 watch 模式。

Tbuild=∑i=1kT(Pi),∀j∈dep(Pi)(j<i)\displaystyle T_{\text{build}} = \sum_{i=1}^{k} T(P_i), \qquad \forall j \in \text{dep}(P_i) \quad (j < i)
总构建时间 = 各子项目构建时间之和,且必须保证被依赖的项目先完成,顺序由拓扑排序保证。
  1. 求和公式总时间等于所有子项目构建时间相加,分灶后各灶位可独立并行。
    ∑i=1kT(Pi)\sum_{i=1}^{k} T(P_i)
  2. 拓扑约束 j < i项目 i 依赖的项目 j 必须排在 i 前面构建。
    ∀j∈dep(Pi)\forall j \in \text{dep}(P_i)
  3. dep(P_i) 依赖集合项目 i 所依赖的兄弟项目集合。
    dep(Pi)\text{dep}(P_i)
  4. 声明文件复用收益被依赖项目输出 .d.ts 后,下游无需重复解析源码,相当于半成品共用。
    T(Pi)≈tdeclare+temitT(P_i) \approx t_{\text{declare}} + t_{\text{emit}}

Watch模式

宴席筹备期间,采购员会不停把新食材送进后厨。如果每次来一根葱都要把整本笔记翻一遍重算,效率依然很低。聪明的后厨会安排一个“哨兵”守在食材柜前:有新食材进来,他喊一嗓子“xx 换了”,相关灶台才动手。这就是 watch 模式。

tsc --watch 会启动两个机制:文件系统监听(fs.watch)捕捉文件变动事件,以及基于 .tsbuildinfo 的增量重建。为了防止你连续按 Ctrl+S 十次就触发十次编译,事件会先进入队列,经过一个短暂的防抖窗口才触发编译——你连续保存三次,它只编译一次。编译完成后,它还会通过内部 API 通知编辑器刷新类型信息,让 IDE 的错误提示几乎实时更新。

哨兵让大厨不用“一分钟重看一遍 20 道菜谱”。不过,不是每家餐厅都用同一套锅具——不同构建工具,就像是不同流派的厨房。接下来我们串门看看。

Tcycle=δdebounce+Tincr\displaystyle T_{\text{cycle}} = \delta_{\text{debounce}} + T_{\text{incr}}
一轮 watch 循环的耗时 = 防抖等待时间 + 一次增量编译耗时。
  1. δ_debounce 防抖延迟事件触发后等待的最短间隔,等编辑动作稳定后再动手。
    δdebounce\delta_{\text{debounce}}
  2. T_incr 增量编译watch 内部复用的正是增量编译,而非全量重建。
    TincrT_{\text{incr}}
  3. T_cycle 单轮周期从文件保存到编辑器中看到新类型提示的总时长。
    TcycleT_{\text{cycle}}

构建工具集成

同样是做菜:传统大锅菜(webpack + ts-loader)讲究原汁原味但慢;快炒店(esbuild/swc)用猛火大铁锅,几分钟出锅却不上妆;中央厨房(babel)负责把半成品切好装盒,不做质量检验;一体化餐厅(vite)直接让快炒师傅掌勺,再单独安排质检员。

webpack 生态里,ts-loader 逐文件用 tsc 转译,类型检查默认串行,很慢;社区常用 fork-ts-checker-webpack-plugin 把类型检查挪到独立进程并行。babel 用 @babel/preset-typescript 只做“剥类型”的转译,速度快但完全不做类型检查。swc 和 esbuild 用 Rust/Go 并行转译,速度比 tsc 快一两个数量级,同样牺牲类型检查。vite 在开发期用 esbuild 预构建依赖,再配合 tsc --noEmit 单独做类型检查。

选哪家厨房取决于你要不要质量检验。但无论哪一派,都有几招通用的“省时秘笈”——下一节我们翻开大厨的锦囊。

S=TtscTtool=Tparse+Tcheck+TemitTtransform\displaystyle S = \frac{T_{\text{tsc}}}{T_{\text{tool}}} = \frac{T_{\text{parse}} + T_{\text{check}} + T_{\text{emit}}}{T_{\text{transform}}}
加速比 = tsc 全流程耗时 ÷ 工具转译耗时;工具可快十倍甚至百倍,代价是类型检查被移出热路径。
  1. S 加速比代表用新式灶台后比传统灶台快多少倍。
    SS
  2. 分子 T_tsctsc 完整走解析、检查、产物三步的耗时。
    Tparse+Tcheck+TemitT_{\text{parse}} + T_{\text{check}} + T_{\text{emit}}
  3. 分母 T_transformesbuild 等只做转译的耗时,少了类型检查环节。
    TtransformT_{\text{transform}}
  4. 权衡关系速度提升来自“不做检查”,所以生产构建仍需 tsc --noEmit 兜底。
    S→大⇒Tcheck→小S \to \text{大} \Rightarrow T_{\text{check}} \to \text{小}

优化策略

大厨的锦囊里装着几条老规矩:隔夜高汤不用再尝咸淡(跳过已封装的第三方库检查);同一种切配交给一人专做(隔离转译);三四个帮厨同时开工(并行构建);做好的成品直接入冰箱(缓存复用)。

具体到配置:skipLibCheck: true 让编译器跳过所有 .d.ts 声明文件的类型检查——第三方库通常在发布前已自检,重复检查纯属浪费。isolatedModules 配合 esbuild/swc/ts-loader 的 transpileOnly 模式时,每个文件独立转译、互不等待,天然适合并行。构建器内置的多进程并行(如 webpack 的 parallel 配置)能充分利用多核 CPU。最后,把 .tsbuildinfo 和 node_modules 一起缓存进 CI,'热启动'能省掉大半时间。

锦囊虽小,样样都管用。但真正到了实战,光有锦囊还不够,还要会“望闻问切”——最后我们来一次后厨全面体检。

Tafter≈Tsrc+ε⋅Tlib,ε→0\displaystyle T_{\text{after}} \approx T_{\text{src}} + \varepsilon \cdot T_{\text{lib}}, \qquad \varepsilon \to 0
优化后耗时只剩源码检查开销,库文件检查开销乘上接近 0 的系数,几乎消失。
  1. T_src 源码耗时工程自身文件的编译开销,这部分无法跳过。
    TsrcT_{\text{src}}
  2. ε·T_lib 库文件开销被跳过检查的声明文件理论耗时,乘以趋近于零的系数。
    ε⋅Tlib\varepsilon \cdot T_{\text{lib}}
  3. T_after 优化后耗时近似等于源码耗时,库检查成本可忽略。
    Tafter≈TsrcT_{\text{after}} \approx T_{\text{src}}

实践与排查

新来的帮厨说“我们这儿编译好慢”,你不能直接信,要拿秒表逐环节测:从备菜到装盘到底卡在哪一步?这就好比用体检报告说话,而不是凭感觉猜病。

tsc --diagnostics 和 --extendedDiagnostics 会输出各阶段的耗时统计:读文件多少毫秒、类型检查多少毫秒、生成代码多少毫秒,甚至内存占用。排查清单依次看:.tsbuildinfo 是否存在且被 CI 缓存(没有则是冷启动);是否误开全量编译(比如构建脚本里 tsc 没带 incremental);依赖图是否过大(大量文件 import 同一个大模块,导致单点被波及面巨大);是否重复编译 node_modules 里的类型。实践上,把 .tsbuildinfo 提交进版本库或 CI 缓存目录,是收益最大的一步。

体检完,你终于明白:慢不是大厨手笨,而是没带记事本、没分灶、没让哨兵盯梢。把这四件事做齐,百人宴会也能从容不迫。

Twarm≈Tgraph+Tincr,Tcold≈N×tf\displaystyle T_{\text{warm}} \approx T_{\text{graph}} + T_{\text{incr}}, \qquad T_{\text{cold}} \approx N \times t_f
有缓存的热启动只需读依赖图加增量编译;没缓存的冷启动退回全量,二者可能相差数十倍。
  1. T_warm 热启动缓存命中时,先读依赖图再增量编译,成本很低。
    Tgraph+TincrT_{\text{graph}} + T_{\text{incr}}
  2. T_cold 冷启动没有 .tsbuildinfo,只能全量重来,接近原始全量耗时。
    N×tfN \times t_f
  3. 对比意义两者差距就是缓存带来的收益,也解释了为何 CI 上增量效果常打折扣。
    Tcold≫TwarmT_{\text{cold}} \gg T_{\text{warm}}

AI 应用场景

大型前端工程 CI 构建加速

阶段:前端工程化与持续集成

解决的问题:传统全量编译在 CI 上每次冷启动要 10 分钟以上;配合 AI 代码生成后,改动频率更高,等待更致命。用项目引用拆分 + 提交 .tsbuildinfo 缓存 + 并行构建,构建时间可降到 1 分钟以内。

具体案例:某团队在 monorepo 中把 UI 组件库、业务页面、工具库拆成 5 个子项目,CI 缓存 .tsbuildinfo 与 node_modules;AI 助手改完代码提交后,流水线增量编译 40 秒完成,全量则要 8 分钟。

编辑器内 AI 编程助手的实时类型反馈

阶段:AI 代码生成与实时校验

解决的问题:Copilot 类工具一次补全几十行代码,若编译器全量检查,IDE 要卡顿数秒才能给出类型错误。TS Server 基于增量编译,让错误标记在几百毫秒内刷新。

具体案例:开发者在 VSCode 中用 Continue/Copilot 生成了一段调用某接口的代码,编辑器的 ts.server 立即沿依赖图做增量检查,红波浪线在 0.5 秒内出现,AI 得以迅速自我修正,而不是把错误带到下一次编译。

微前端多团队并行交付

阶段:工程架构与并行开发

解决的问题:多团队同一仓库互相影响,全量构建互相拖累。项目引用把各业务域拆成独立子项目,AI 生成的各模块代码独立增量编译、独立发布。

具体案例:电商中台把商品、订单、用户三个域拆成三个 project,每个子项目独立 watch;AI 改完订单域只编译订单域及其引用方,其余模块零感知,各团队各自验收半成品。

Python 演示

python70 行
"""Simulate incremental compilation: only rebuild changed files and dependents."""import hashlib  class IncrementalCompiler:    def __init__(self, source_map):        self.source_map = dict(source_map)        self.dep_map = {}           # 被依赖文件 -> 依赖它的文件列表        self.cache = {}             # 文件路径 -> 内容哈希(大厨的记事本)        self.compile_count = 0      # 实际重写文件的次数     def register_deps(self, file, deps):        # 记录 file 依赖了 deps 中的哪些文件        for dep in deps:            self.dep_map.setdefault(dep, []).append(file)     def digest(self, path):        # 计算文件当前内容的指纹(哈希)        return hashlib.md5(self.source_map[path].encode()).hexdigest()     def is_stale(self, path):        # 内容哈希与笔记不一致 => 文件变了        return self.cache.get(path) != self.digest(path)     def rebuild(self, path):        # 重新“编译”并更新笔记        self.cache[path] = self.digest(path)        self.compile_count += 1     def plan(self):        # 找出变更文件,并沿依赖图收集下游        queue, seen = [], set()        for path in self.source_map:            if self.is_stale(path) and path not in seen:                seen.add(path)                queue.append(path)        for path in list(queue):            for downstream in self.dep_map.get(path, []):                if downstream not in seen:                    seen.add(downstream)                    queue.append(downstream)        return queue     def run(self, full=False):        # full=True 模拟全量编译;否则只处理变更及下游        jobs = list(self.source_map) if full else self.plan()        rebuilt = 0        for path in jobs:            if self.is_stale(path):                self.rebuild(path)                rebuilt += 1        return rebuilt, len(jobs)  if __name__ == "__main__":    src = {}    for i in range(8):        src["a%d.ts" % i] = "export const v%d = %d;" % (i, i)    src["app.ts"] = "import {v0} from './a0'"    c = IncrementalCompiler(src)    # app.ts / a1 / a4 / a7 都依赖 a0.ts    c.register_deps("app.ts", ["a0.ts"])    c.register_deps("a1.ts", ["a0.ts"])    c.register_deps("a4.ts", ["a0.ts"])    c.register_deps("a7.ts", ["a0.ts"])    print("cold start : rebuilt %d, checked %d" % c.run(full=True))    c.source_map["a0.ts"] = "export const v0 = 100;"   # 模拟修改 a0    rebuilt, checked = c.run(full=False)    print("incremental: rebuilt %d, checked %d" % (rebuilt, checked))    print("total real rebuilds:", c.compile_count)
目的:模拟 tsc 增量编译的核心决策——通过内容哈希找出变更文件,沿依赖图收集下游,只重做必要的工作。运行后可以看到冷启动要重建 9 个文件,而修改 a0 后增量模式只检查 5 个、实际只重写 1 个。
  1. self.dep_map = {}记录“谁依赖谁”的关系表,相当于菜谱间的关系网。
  2. return hashlib.md5(self.source_map[path].encode()).hexdigest()用哈希做“食材指纹”,内容一变指纹就变。
  3. if self.is_stale(path) and path not in seen:发现变更文件,并把它们排入待处理队列。
  4. for downstream in self.dep_map.get(path, []):遍历下游依赖,把受波及的文件也纳入检查范围。
  5. jobs = list(self.source_map) if full else self.plan()全量与增量的分水岭:全量处理所有文件,增量只处理计划队列。
  6. c.register_deps("a1.ts", ["a0.ts"])注册依赖关系,告诉编译器 a1 依赖 a0。
  7. c.source_map["a0.ts"] = "export const v0 = 100;"模拟真实开发中的一次修改。
  8. print("incremental: rebuilt %d, checked %d" % (rebuilt, checked))输出证明:检查面变小,真实重建更少。

常见误区

误区增量编译嘛,就是哪个文件变了就编译哪个文件,其他文件完全不用管,所以才会这么快。

正解编译器必须沿依赖图检查“下游”:你改了 a.ts 的接口,import 它的 b.ts 类型可能不匹配,必须重新检查。好比面粉换了配方,包子馅、面条面团都要重新验货;不过验完发现内容没变,就不会重新生成产物。所以增量编译的检查面是“变更 + 下游”,只是产出面变小。

为何常见:“改哪个编哪个”太符合直觉了,容易忽略类型系统是跨文件的全局约束。

误区我用了 `tsc --watch`,编译飞快,这肯定就是增量编译了吧,不用再单独配置 incremental。

正解watch 是“触发机制”,增量编译是“省力机制”,二者是配套关系:watch 靠文件监听发现变化,再调用增量编译来避免全量。如果没开 incremental,watch 每次仍会全量重建——就像哨兵喊了“菜换了”,灶台师傅却把整桌菜重做一遍。

为何常见:watch 模式在工程里总是显得快,大家把功劳记在监听头上,实际省时的是缓存机制。

误区`.tsbuildinfo` 不就是个缓存吗?缓存删了会自动重新生成,删掉也没关系,还能保持仓库干净。

正解生成当然可以,但删掉等于把冷启动的几十秒甚至几分钟还回来。增量编译的全部收益都在这本“工作笔记”里;它应当进入 CI 缓存甚至版本库,让流水线热启动。笔记可以重抄,但重抄的那一晚你又要加班。

为何常见:大家习惯了 node_modules 缓存可删,误以为所有缓存都一样可有可无。

误区项目引用就是把工程拆成多个 tsconfig.json,每个文件夹一份配置文件就行。

正解项目引用必须有三个要素:根配置用 references 列出子项目、子项目开启 composite: true、按依赖拓扑序构建。只拆 tsconfig 不配 references,tsc 根本不会联动,等于分了灶却没安排出菜顺序,各灶还是各自为战。

为何常见:“拆分”两个字太好理解了,但复合配置的协同机制才是关键,初学者常忽略它。

误区既然增量编译这么快,那生产环境的构建也一直开增量,肯定能提速。

正解增量编译的目标是“改哪里验哪里”,但首次构建一定是全量;当依赖图极其复杂、一次改动波及半个工程时,增量收益会缩水。生产构建常要求干净环境,缓存不可用时只能冷启动。它是最优策略,不是魔法。

为何常见:宣传里只讲热点路径的省时,忽略了冷启动和波及面放大的现实。

面试题

Q1. 增量编译的“记事本”在 TypeScript 中对应哪个文件?

单选题 · easy

  • A.tsconfig 配置文件
  • B.构建缓存记录文件← 正确答案
  • C.依赖锁定清单文件
  • D.编译入口清单文件

答案:B。增量编译的缓存对应 .tsbuildinfo 文件,它记录文件哈希、依赖关系和编译状态。tsconfig 是配置,锁定清单属于包管理器,入口清单不是真实产物。

Q2. 项目第一次执行增量编译时,表现通常是?

单选题 · easy

  • A.与全量编译耗时相当← 正确答案
  • B.完全跳过类型检查过程
  • C.只编译程序入口文件
  • D.不产出任何编译结果

答案:A。首次编译没有 .tsbuildinfo 缓存,相当于大厨手里没有记事本,所有菜都得重做,所以耗时与全量编译相当。缓存建立后,后续编译才会变快。

Q3. 修改 a.ts 中一个接口后,import 它的 b.ts 也会被重新检查,原因是?

单选题 · easy

  • A.b.ts 文件内容随之改变
  • B.b.ts 的类型可能受影响← 正确答案
  • C.b.ts 的格式需要重排
  • D.编译器尚未更新记录

答案:B。类型检查是跨文件的全局约束:a.ts 的接口变了,引用它的 b.ts 可能类型不匹配。好比面粉配方变了,包子馅也要重新验货;b.ts 的内容本身没变,但它的“安全性”需要重新确认。

Q4. 开启项目引用时,被引用的子项目必须开启哪项配置?

单选题 · mid

  • A.复合构建选项开关← 正确答案
  • B.错误即停产物开关
  • C.严格空值检查开关
  • D.模块互操作开关

答案:A。项目引用要求子项目开启 composite: true,它自动启用增量编译并强制生成声明文件。错误即停、严格空值、模块互操作都是独立功能,与项目引用的联动机制无关。

Q5. skipLibCheck 的主要作用是跳过哪类文件的类型检查?

单选题 · mid

  • A.工程内手写的源文件
  • B.第三方类型声明文件← 正确答案
  • C.编译后生成的产物文件
  • D.工程内项目的配置文件

答案:B。skipLibCheck 跳过所有 .d.ts 声明文件(主要来自第三方库)的检查,因为这些库发布前已自检过。好比隔夜高汤不用再尝咸淡。源码、产物、配置都不属于被跳过的对象。

Q6. watch 模式与增量编译的关系是?

单选题 · mid

  • A.两者互斥只能二选一
  • B.监听模式依赖增量机制← 正确答案
  • C.监听可完全取代增量
  • D.增量必须依托监听模式

答案:B。watch 负责“发现变化”(文件监听 + 防抖),增量编译负责“省力重建”,两者是配套关系。没有 watch,增量编译也能在命令行手动触发;没有增量编译,watch 每次只能全量重建。

Q7. 在 CI 流水线上,增量编译的收益常不如本机明显,最可能的原因是?

单选题 · extreme

  • A.CI 缺少持久构建缓存← 正确答案
  • B.CI 不需要做类型检查
  • C.本机的增量机制更先进
  • D.CI 网络延迟拖慢编译

答案:A。增量收益依赖 .tsbuildinfo 缓存。如果 CI 没有持久化缓存目录,每次流水线都是冷启动,等于全量编译。好比大厨换了新厨房,记事本没带过去,一切只能从头做。

Q8. 下列哪些做法能有效缩短 TypeScript 编译耗时?

多选题 · mid

  • A.开启跳过声明文件检查← 正确答案
  • B.用项目引用拆分工程← 正确答案
  • C.启用增量编译复用记录← 正确答案
  • D.删除源码的类型标注

答案:ABC。skipLibCheck 减少检查面,项目引用实现分灶并行,增量缓存避免重复劳动,三者都有效。删除类型标注虽然能提速,但破坏了 TypeScript 的核心价值,不能被当作优化手段。

Q9. 关于项目引用的构建顺序,下列说法正确的有?

多选题 · hard

  • A.被依赖项目理应先行← 正确答案
  • B.顺序按依赖图排列← 正确答案
  • C.各项目可无序并行
  • D.前序产物供后续用← 正确答案

答案:ABD。项目引用按拓扑序构建:被依赖项目必须先行,顺序由依赖图决定,前序项目产出的声明文件供后序项目消费。若允许无序并行,后序项目会读到过期或不存在的声明文件。

Q10. 下列哪些情况会导致增量编译失效或收益大幅缩水?

多选题 · hard

  • A.缓存记录文件被删除← 正确答案
  • B.切换分支改动幅度极大← 正确答案
  • C.修改编译选项导致缓存失效← 正确答案
  • D.保存文件后立即构建

答案:ABC。缓存删除后只能冷启动;分支切换若改动波及半个工程,增量退化为接近全量;修改编译选项也会使旧缓存失效。而保存后立即构建正是增量编译的典型使用场景,不会导致失效。