| name | tsc-trace-analyzer |
| description | Run and analyze TypeScript compiler traces using --generateTrace. Identifies slow type-checking operations, maps them to source code, and suggests concrete fixes. Use when users mention slow tsc builds, TypeScript performance, type-checking bottlenecks, or compilation speed. |
tsc-trace-analyzer
Run tsc --generateTrace on a TypeScript project, parse the resulting trace files, identify compilation bottlenecks, and suggest concrete code fixes.
When to use
Use this skill whenever the user complains that the TypeScript compiler or compilation is slow, or when they want to investigate where tsc is spending time. Triggers include — but are not limited to — any of the following:
- "The TypeScript compiler is slow" / "tsc is slow" / "compilation is slow"
- "My TypeScript build is slow" / "the build takes forever"
- "Type checking is slow" / "type checking takes forever" / "tsc takes forever"
- "Why is my TypeScript project so slow to compile?"
- "How do I make tsc faster?" / "How do I speed up TypeScript?"
- "Find TypeScript performance bottlenecks" / "Find type-checking hotspots"
- "Profile the TypeScript compiler" / "profile tsc"
- Any mention of
--generateTrace, trace.json, types.json, "tsc trace", or "TypeScript tracing"
tsc is crashing with a JavaScript heap out-of-memory error and the user wants to understand or fix it
- The user wants to know what the TypeScript compiler is spending its time on
- The user asks for help reducing TypeScript compile times, build times, or CI times caused by
tsc
Also use it proactively if the user shares a slow tsc build log, a long type-checking duration, or asks for advice on TypeScript performance optimization.
Instructions
Follow these steps in order:
Step 1: Find tsconfig.json
Search for tsconfig.json files in the project:
- Use Glob to find
**/tsconfig.json files, excluding node_modules
- If exactly one is found, use it
- If multiple are found, pick by this priority order:
- Root
tsconfig.json at the project root — use it if present
src/tsconfig.json — use it if there is no root tsconfig
- Otherwise, ask the user which one to analyze
- If none are found, tell the user this skill requires a TypeScript project with a
tsconfig.json
Step 2: Generate the trace
Use these two fixed paths (no variables, no command substitution):
- Trace output:
/tmp/tsc-trace-analyzer-trace
- Emit output:
/tmp/tsc-trace-analyzer-emit
First, clear any previous run:
rm -rf /tmp/tsc-trace-analyzer-trace /tmp/tsc-trace-analyzer-emit
Then run the compiler with tracing enabled. Run it as a single, plain command — one line, no pipes, no redirections, no && chaining, no $? / exit-code echoes:
npx tsc -p <tsconfig_path> --generateTrace /tmp/tsc-trace-analyzer-trace --outDir /tmp/tsc-trace-analyzer-emit --incremental false
Shell hygiene (important): do NOT wrap the command in 2>&1 | tail -N, do NOT chain it with && echo "EXIT:$?", do NOT use $(…) command substitution, and do NOT combine multiple commands with && on one line. Keep every command flat and standalone. If tsc produces a lot of output, that's fine — just let it through.
Important flags:
--incremental false ensures a full compilation (incremental builds skip files and produce incomplete traces)
--outDir /tmp/tsc-trace-analyzer-emit redirects emitted .js (and .d.ts) files to a temp directory so we don't pollute the project. Do NOT use --noEmit — skipping emit makes the emit phase appear as 0ms in the trace and produces a profile that doesn't match a real production build.
- If the project uses a build tool wrapper (e.g.,
vue-tsc, tspc), use the appropriate compiler binary instead of npx tsc
After analysis is complete, the temp emit directory can be deleted with rm -rf /tmp/tsc-trace-analyzer-emit. Don't delete it during analysis — some hotspot investigations may want to inspect the emitted output.
If the compilation fails with errors, that's OK — the trace is still generated and useful. Inform the user that compilation errors exist but proceed with the analysis.
If the trace fails with a memory error (V8 stack traces, "JavaScript heap out of memory", "FATAL ERROR: Reached heap limit"), retry with more heap using the npx --node-options form (this stays a single plain command, no env-var prefix):
npx --node-options=--max-old-space-size=8192 tsc -p <tsconfig_path> --generateTrace /tmp/tsc-trace-analyzer-trace --outDir /tmp/tsc-trace-analyzer-emit --incremental false
Bump higher (e.g. 12288, 16384) if 8GB still isn't enough on very large projects. The flag changes nothing about the trace output — it just gives V8 enough memory to finish.
Step 3: Run the analyzer
Run the helper script to parse and aggregate the trace data. The script is plain ESM with zero dependencies, so it runs unchanged on Node.js, Bun, or Deno. Try them in order:
Each of the commands below is a single plain command — no pipes, no redirections, no chaining.
Preferred — Node.js:
node <SKILL_DIR>/scripts/analyze-trace.mjs /tmp/tsc-trace-analyzer-trace --top 15 --cwd <project_root>
Fallback — Bun (if node is not on PATH):
bun run <SKILL_DIR>/scripts/analyze-trace.mjs /tmp/tsc-trace-analyzer-trace --top 15 --cwd <project_root>
Fallback — Deno (if neither node nor bun is available):
deno run --allow-read --allow-env <SKILL_DIR>/scripts/analyze-trace.mjs /tmp/tsc-trace-analyzer-trace --top 15 --cwd <project_root>
Check availability with command -v node, command -v bun, command -v deno. If none of the three are installed, tell the user this skill requires Node.js, Bun, or Deno to run the analyzer.
Where <SKILL_DIR> is the directory containing this SKILL.md file.
The script outputs a JSON object with:
summary: Total time broken down by phase (parse, bind, check, emit)
hotspotsByFile: Files ranked by total check time
hotspotsBySpan: Individual operations ranked by duration, with source locations and code snippets
duplicatePackages: npm packages found at multiple paths in node_modules
Step 4: Read and analyze results
- Parse the JSON output from the script
- For each hotspot in
hotspotsBySpan, read the relevant source file at the indicated location to understand the full code context
- Identify which fix patterns (see reference below) apply to each hotspot
Step 5: Present findings
Format the results as shown below. Color the phase times in the summary line using ANSI escape codes so they stand out in the terminal: green for fast phases (<10% of total), yellow for moderate (10–40%), red for hot phases (>40%). Also color individual phase labels consistently — Parse (cyan), Bind (magenta), Check (yellow), Emit (blue) — so the audience can track them at a glance during a demo.
Use these ANSI codes (wrap each value):
- Reset:
\033[0m
- Bold:
\033[1m
- Red:
\033[31m, Green: \033[32m, Yellow: \033[33m, Blue: \033[34m, Magenta: \033[35m, Cyan: \033[36m
Example colored summary line (literal output, terminal will render colors):
Total: 35.0s | Parse: 2.8s Bind: 2.5s Check: 29.7s Emit: 0.0s
^bold ^cyan ^magenta ^yellow+RED ^blue
The format itself:
## TypeScript Compiler Performance Analysis
**Total compilation time:** <colored>Xs</colored> (Parse: <cyan>Xs</cyan> | Bind: <magenta>Xs</magenta> | Check: <colored>Xs</colored> | Emit: <blue>Xs</blue>)
**Files analyzed:** N
### Top Bottlenecks
| # | Duration | File | Location | Operation | Issue |
|---|----------|------|----------|-----------|-------|
| 1 | X,XXXms | path | L##-## | eventName | brief description |
| ... |
### Detailed Analysis
#### 1. path/to/file.ts:L##-## (X,XXXms)
[Explanation of what's slow and why]
**Current code:**
[relevant code snippet]
**Suggested fix:**
[concrete code change with explanation]
When emitting the summary line specifically, print it as a raw line with ANSI codes (not inside a markdown code block) so the terminal renders the colors. The table and detailed analysis sections remain plain markdown.
If duplicatePackages is non-empty, add a section about duplicate packages and recommend npm dedupe.
Step 6: Suggest fixes
For each hotspot, propose concrete code changes based on the patterns below. Show before/after code. Don't auto-apply changes — present them for the user to review.
Fix Patterns Reference
The patterns below are drawn from TypeScript's official performance guide. When presenting them to the user, keep the guidance aligned with that wiki rather than introducing opinions.
When analyzing hotspots, match them against these common TypeScript performance antipatterns:
1. Deeply nested conditional types
Symptom: structuredTypeRelatedTo or checkExpression is slow on conditional type expressions.
Fix: Break A extends B ? (C extends D ? ...) : ... into named type aliases at each level:
type Result<T> = T extends A ? (T extends B ? X : Y) : (T extends C ? Z : W);
type HandleA<T> = T extends B ? X : Y;
type HandleOther<T> = T extends C ? Z : W;
type Result<T> = T extends A ? HandleA<T> : HandleOther<T>;
2. Large union types
Symptom: structuredTypeRelatedTo comparing unions with many members (>20).
Fix: Use discriminated unions with a kind field, or break into smaller union groups:
type Event = ClickEvent | HoverEvent | ScrollEvent | ... (50+ members)
type Event = { kind: 'click'; ... } | { kind: 'hover'; ... } | ...
3. Missing return type annotations
Symptom: checkExpression is slow on function bodies, especially exported or generic functions.
Fix: Add explicit return type annotations so TypeScript can skip inference:
export function processData(items: Item[]) {
}
export function processData(items: Item[]): ProcessedResult {
}
4. Deep generic nesting
Symptom: getVariancesWorker or structuredTypeRelatedTo with deeply nested generics.
Fix: Flatten into intermediate named types:
type Deep = Promise<Array<Map<string, Set<Record<string, T>>>>>;
type InnerMap = Map<string, Set<Record<string, T>>>;
type Deep = Promise<Array<InnerMap>>;
5. type aliases for object shapes
Symptom: Slow structuredTypeRelatedTo between object types defined with type.
Fix: Use interface instead — interfaces are cached by name and compared faster:
type User = { id: string; name: string; ... };
interface User { id: string; name: string; ... }
6. Mapped types over large key sets
Symptom: Slow check events involving { [K in keyof LargeType]: ... }.
Fix: Use Pick<T, RelevantKeys> to reduce the mapped surface:
type Partial<T> = { [K in keyof T]?: T[K] };
type RelevantPartial = Partial<Pick<LargeType, 'key1' | 'key2' | 'key3'>>;
7. Duplicate node_modules packages
Symptom: duplicatePackages array is non-empty in analyzer output.
Fix: Run npm dedupe or fix version conflicts. Multiple copies of the same package means TypeScript checks types from each copy separately.
8. Large projects without project references
Symptom: High overall checkMs across many files (>500 files, >10s check time).
Fix: Split into project references with composite: true for incremental checking:
{
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/ui" }
]
}
9. Checking declaration files from node_modules
Symptom: Files in node_modules appear in hotspotsByFile.
Fix: Enable "skipLibCheck": true in tsconfig.json to skip checking .d.ts files.
10. Barrel file re-exports
Symptom: index.ts barrel files appear in hotspots with high check times.
Fix: Replace barrel re-exports with direct imports:
import { Button } from '@/components';
import { Button } from '@/components/Button';
11. The work doesn't belong in the type system at all
Symptom: A single hotspot is driven by a clever-but-exotic type — deeply recursive template literal types, brand-checks like IsUpperSnakeCase<S>, arithmetic-on-types, parsers encoded as conditional types, etc. These often show up as extremely long structuredTypeRelatedTo / instantiateType chains on tiny amounts of source code.
Fix: Be honest with the user: TypeScript can express these checks, but it's not the right tool for them. Recommend moving the constraint to a more appropriate layer and keeping the TypeScript side simple (string, number, a plain interface, etc.). Typical alternatives:
- A lint rule (ESLint custom rule, Biome) for format/shape constraints on string literals, naming conventions, file layouts, etc.
- Runtime validation (Zod, Valibot, ArkType,
io-ts) for data coming from outside the program — these give you runtime guarantees AND a derived TS type for free.
- Code generation (e.g. generating types from an OpenAPI schema, SQL schema, protobuf) instead of deriving them from types at compile time.
- A unit test for invariants that only need to hold for a small, known set of values.
Example — validating UPPER_SNAKE_CASE identifiers:
type IsUpperSnake<S extends string> = S extends `${infer C}${infer Rest}`
? C extends Uppercase<C> & ('A'|'B'|'C'||'Z'|'_'|'0'||'9')
? IsUpperSnake<Rest>
: false
: true;
function defineEvent<N extends string>(name: N & (IsUpperSnake<N> extends true ? N : never)) { }
function defineEvent(name: string) { }
Only suggest this when the hotspot clearly comes from a type-level trick rather than from ordinary application code. Don't recommend ripping out types that are doing useful, idiomatic work.