🔎 Search Terms
incremental noEmit slow first edit, tsbuildinfo signature, declaration emit getAlternativeContainingModules getAliasForSymbolInContainer, isSymbolAccessible slow, affected files signature, typescript 7 incremental slower than full check
🕗 Version & Regression Information
⏯ Playground Link
No response
💻 Code
The problem needs thousands of files and build state across runs, so here is a generator instead of a snippet. hub.ts stands for a widely imported constants module. Each leaf exports a value whose inferred type names an interface that the leaf never imports.
// generate.js — node generate.js <dir> [leaves=6000] [types=3000]
const fs = require('fs');
const path = require('path');
const dir = path.resolve(process.argv[2] || 'repro');
const LEAVES = Number(process.argv[3] || 6000);
const TYPES = Number(process.argv[4] || 3000);
fs.rmSync(path.join(dir, 'src'), {recursive: true, force: true});
fs.mkdirSync(path.join(dir, 'src/types'), {recursive: true});
fs.mkdirSync(path.join(dir, 'src/leaves'), {recursive: true});
const write = (file, text) => fs.writeFileSync(path.join(dir, 'src', file), text);
write('hub.ts', 'export const HUB_PREFIX = "hub";\nexport const HUB_LIMIT = 10;\n');
for (let i = 0; i < TYPES; i++) {
write(`types/t${i}.ts`,
`export interface Model${i} { id: string; value${i}: number; }\n` +
`export function make${i}(id: string): Model${i} { return {id, value${i}: ${i}}; }\n`);
}
write('types/index.ts', Array.from({length: TYPES}, (_, i) => `export {make${i}} from './t${i}';\n`).join(''));
for (let i = 0; i < LEAVES; i++) {
const a = i % TYPES;
const b = (i * 7 + 3) % TYPES;
write(`leaves/l${i}.ts`,
`import {HUB_PREFIX} from '../hub';\n` +
`import {make${a}, make${b}} from '../types';\n` +
`export const leaf${i} = {first: make${a}(HUB_PREFIX), second: make${b}('${i}')};\n`);
}
write('index.ts', Array.from({length: LEAVES}, (_, i) => `export * from './leaves/l${i}';\n`).join(''));
fs.writeFileSync(path.join(dir, 'tsconfig.json'), JSON.stringify({
compilerOptions: {
target: 'es2022', module: 'esnext', moduleResolution: 'bundler', strict: true,
noEmit: true, incremental: true, skipLibCheck: true,
},
include: ['src'],
}, null, 2));
node generate.js repro && cd repro
rm -f tsconfig.tsbuildinfo
tsc -p . --extendedDiagnostics --incremental false # full check
tsc -p . --extendedDiagnostics # clean incremental build
echo '// comment-only edit' >> src/hub.ts
tsc -p . --extendedDiagnostics # first edit: slow
echo '// another comment-only edit' >> src/hub.ts
tsc -p . --extendedDiagnostics # second edit: fast again
Wall times on a 32-core Linux machine (6000 leaves, 3000 types):
|
5.4.5 |
6.0.3 |
7.0.2 |
7.1.0-dev.20260925.1 |
full check (--incremental false) |
3.0 s |
3.2 s |
0.57 s |
0.56 s |
| clean incremental build |
3.6 s |
3.3 s |
0.83 s |
0.82 s |
| no-op rebuild |
2.2 s |
2.0 s |
0.59 s |
0.61 s |
first comment-only edit to hub.ts |
31.7 s |
24.5 s |
34.3 s |
41.8 s |
| second comment-only edit |
2.5 s |
2.3 s |
0.73 s |
0.74 s |
for comparison: --incremental false --noEmit false --declaration --emitDeclarationOnly --outDir out |
|
23.9 s |
8.2 s |
11.4 s |
The cost grows faster than the program: with 7.0.2, the first edit takes 4.1 s at 1500 leaves, 8.5 s at 3000 and 34.3 s at 6000.
🙁 Actual behavior
After a clean incremental build, the first edit to a module that many files import takes much longer than checking the whole program from scratch, even when the edit is a comment. In 7.0.2 the time is reported as Check time, and in the nightly as Emit time.
A CPU profile of that run (--pprofDir) shows where it goes. The builder emits declarations for the edited file's importers to compute their shape signatures, even with --noEmit. Serializing the inferred types asks whether each named symbol is accessible, and getAlternativeContainingModules then scans every source file for a module that re-exports the symbol:
flat flat% cum cum%
2.54s 6.66% 30.28s 79.45% checker.(*Checker).getAlternativeContainingModules
1.93s 5.06% 24.93s 65.42% checker.(*Checker).getAliasForSymbolInContainer
2.71s 7.11% 2.71s 7.11% ast.IsNonLocalAlias
0 0% 32.78s 86.01% declarations.(*DeclarationTransformer).ensureType
Duration: 34.20s, Total samples = 38.11s (111.42%)
The run uses about one core: 111% CPU, against about 700% for a full check. The same declaration emit run on its own (--emitDeclarationOnly) takes 8.2 s, so running it for signatures costs about 4× as much.
After that first edit the .tsbuildinfo holds real declaration signatures, and later edits are fast again. That includes edits that change the hub's exports, and edits to other widely imported modules.
🙂 Expected behavior
The first incremental rebuild after a comment-only edit should cost about as much as a full check, not 8–75× more. At least, computing signatures should not scan the whole program once per symbol, and it should run in parallel the way checking does.
Additional information about the issue
This is how it shows up in a real application with 22,154 files (a React + Express monorepo, TypeScript 7.0.2, tsc -p src/ui --noEmit --incremental). After a clean build, the first comment-only edit to a 112-line constants module takes 291 s. That module is re-exported through a shared barrel that about 900 files import. A full check of the same program takes 21 s and the no-op rebuild 3.5 s. The next edit takes 4.7 s. On the warm .tsbuildinfo, an edit that adds an export to that module takes 24 s.
The profile of that run matches the synthetic one: 58% in getAlternativeContainingModules, 136% CPU over 303 s.
flat flat% cum cum%
57.08s 13.80% 57.08s 13.80% ast.IsNonLocalAlias
15.43s 3.73% 242.09s 58.54% checker.(*Checker).getAlternativeContainingModules
14.86s 3.59% 217.74s 52.65% checker.(*Checker).getAliasForSymbolInContainer
0 0% 228.96s 55.37% declarations.(*DeclarationTransformer).ensureType
Duration: 302.67s, Total samples = 413.53s (136.63%)
Separately, tsc --watch --noEmit on that project took 437 s for its first check and peaked at 13 GB; I haven't profiled that run. For now we run our dev-server type checks with incremental off. A five-minute wait after every clean build costs more than faster rebuilds save afterwards.
#63830 reports the same hotspot in declaration emit for tsc -b on a private codebase and is waiting on a shareable repro. The generator above reproduces it without private code. I can attach the raw pprof files.
🔎 Search Terms
incremental noEmit slow first edit, tsbuildinfo signature, declaration emit getAlternativeContainingModules getAliasForSymbolInContainer, isSymbolAccessible slow, affected files signature, typescript 7 incremental slower than full check
🕗 Version & Regression Information
⏯ Playground Link
No response
💻 Code
The problem needs thousands of files and build state across runs, so here is a generator instead of a snippet.
hub.tsstands for a widely imported constants module. Each leaf exports a value whose inferred type names an interface that the leaf never imports.Wall times on a 32-core Linux machine (6000 leaves, 3000 types):
--incremental false)hub.ts--incremental false --noEmit false --declaration --emitDeclarationOnly --outDir outThe cost grows faster than the program: with 7.0.2, the first edit takes 4.1 s at 1500 leaves, 8.5 s at 3000 and 34.3 s at 6000.
🙁 Actual behavior
After a clean incremental build, the first edit to a module that many files import takes much longer than checking the whole program from scratch, even when the edit is a comment. In 7.0.2 the time is reported as
Check time, and in the nightly asEmit time.A CPU profile of that run (
--pprofDir) shows where it goes. The builder emits declarations for the edited file's importers to compute their shape signatures, even with--noEmit. Serializing the inferred types asks whether each named symbol is accessible, andgetAlternativeContainingModulesthen scans every source file for a module that re-exports the symbol:The run uses about one core: 111% CPU, against about 700% for a full check. The same declaration emit run on its own (
--emitDeclarationOnly) takes 8.2 s, so running it for signatures costs about 4× as much.After that first edit the
.tsbuildinfoholds real declaration signatures, and later edits are fast again. That includes edits that change the hub's exports, and edits to other widely imported modules.🙂 Expected behavior
The first incremental rebuild after a comment-only edit should cost about as much as a full check, not 8–75× more. At least, computing signatures should not scan the whole program once per symbol, and it should run in parallel the way checking does.
Additional information about the issue
This is how it shows up in a real application with 22,154 files (a React + Express monorepo, TypeScript 7.0.2,
tsc -p src/ui --noEmit --incremental). After a clean build, the first comment-only edit to a 112-line constants module takes 291 s. That module is re-exported through a shared barrel that about 900 files import. A full check of the same program takes 21 s and the no-op rebuild 3.5 s. The next edit takes 4.7 s. On the warm.tsbuildinfo, an edit that adds an export to that module takes 24 s.The profile of that run matches the synthetic one: 58% in
getAlternativeContainingModules, 136% CPU over 303 s.Separately,
tsc --watch --noEmiton that project took 437 s for its first check and peaked at 13 GB; I haven't profiled that run. For now we run our dev-server type checks withincrementaloff. A five-minute wait after every clean build costs more than faster rebuilds save afterwards.#63830 reports the same hotspot in declaration emit for
tsc -bon a private codebase and is waiting on a shareable repro. The generator above reproduces it without private code. I can attach the raw pprof files.