🔎 Search Terms
declaration emit performance, declaration diagnostics, allocations, setupDiagnosticContext, createGetSymbolAccessibilityDiagnosticForNode, noEmit declaration
🕗 Version & Regression Information
- Observed on
main (7.1.0-dev, aa81492, 2026-10-09).
⏯ Playground Link
Not reproducible in the playground; it needs a generated project. Generator below.
💻 Code
node gen.mjs && tsgo -p . --extendedDiagnostics
// Ordinary files with many exported declarations, checked with `declaration: true` so that declaration
// diagnostics run the declaration transformer over every declaration.
import fs from "node:fs";
const FILES = 1500, CLASSES_PER_FILE = 6;
fs.mkdirSync("src", { recursive: true });
for (let f = 0; f < FILES; f++) {
let text = "";
for (let c = 0; c < CLASSES_PER_FILE; c++) {
const n = `${f}_${c}`;
text += `export interface Options${n} { name: string; size?: number; tags: readonly string[] }
export class Service${n}<T extends object = {}> {
private readonly items = new Map<string, T>();
static readonly kind = "service${n}" as const;
constructor(public readonly options: Options${n}, protected limit = 10) {}
get count() { return this.items.size; }
set count(value: number) { this.limit = value; }
add(key: string, item: T, overwrite = false) { if (overwrite || !this.items.has(key)) this.items.set(key, item); return this; }
find(predicate: (item: T, key: string) => boolean) { for (const [key, item] of this.items) if (predicate(item, key)) return { key, item }; return undefined; }
map<U>(fn: (item: T) => U) { return [...this.items.values()].map(fn); }
}
export function create${n}(options: Options${n}) { return new Service${n}(options); }
export const defaults${n} = { name: "${n}", size: ${c}, tags: ["a", "b"] as const };
export type Item${n} = ReturnType<typeof create${n}> extends Service${n}<infer T> ? T : never;
`;
}
fs.writeFileSync(`src/file${f}.ts`, text);
}
fs.writeFileSync("tsconfig.json", JSON.stringify({ compilerOptions: { target: "es2022", module: "esnext", moduleResolution: "bundler", strict: true, declaration: true, noEmit: true, types: [], lib: ["es2022"] }, include: ["src"] }));
🙁 Actual behavior
14.5M allocations for a 1,557-file program with no errors.
Every declaration the declaration transformer visits installs a diagnostic context: setupDiagnosticContext calls createGetSymbolAccessibilityDiagnosticForNode, which allocates a closure that picks the message to report if a symbol used by the declaration turns out to be inaccessible. It also returns a second closure that restores the previous context. The selector is only invoked when such an error is actually reported, which almost never happens.
This runs for every emitted declaration file, and under noEmit with declaration enabled for every file's declaration diagnostics. In an 8.6k-file package of a private monorepo, the two closures were 11% of all allocations of a --noEmit check.
🙂 Expected behavior
Establishing a diagnostic context should not allocate; the selector only needs to exist once an error is reported.
Additional information about the issue
With the change in the linked PR, the program above makes 8.7M allocations (−40%) and the monorepo check makes 11% fewer. Emitted declarations and diagnostics are unchanged.
The profiling and the patch were done with Claude Code.
🔎 Search Terms
declaration emit performance, declaration diagnostics, allocations, setupDiagnosticContext, createGetSymbolAccessibilityDiagnosticForNode, noEmit declaration
🕗 Version & Regression Information
main(7.1.0-dev, aa81492, 2026-10-09).⏯ Playground Link
Not reproducible in the playground; it needs a generated project. Generator below.
💻 Code
node gen.mjs && tsgo -p . --extendedDiagnostics🙁 Actual behavior
14.5M allocations for a 1,557-file program with no errors.
Every declaration the declaration transformer visits installs a diagnostic context:
setupDiagnosticContextcallscreateGetSymbolAccessibilityDiagnosticForNode, which allocates a closure that picks the message to report if a symbol used by the declaration turns out to be inaccessible. It also returns a second closure that restores the previous context. The selector is only invoked when such an error is actually reported, which almost never happens.This runs for every emitted declaration file, and under
noEmitwithdeclarationenabled for every file's declaration diagnostics. In an 8.6k-file package of a private monorepo, the two closures were 11% of all allocations of a--noEmitcheck.🙂 Expected behavior
Establishing a diagnostic context should not allocate; the selector only needs to exist once an error is reported.
Additional information about the issue
With the change in the linked PR, the program above makes 8.7M allocations (−40%) and the monorepo check makes 11% fewer. Emitted declarations and diagnostics are unchanged.
The profiling and the patch were done with Claude Code.