Find out why TypeScript is slow on your project, with fixes proven on your own code.
Quick start · What you get · Commands · Benchmarks · How it works · CI · FAQ
npx typescript-perfNo config, no plugin, nothing written to your project. typescript-perf runs the TypeScript your project already uses (5.x, 6.0 or 7.0) through its command line, measures it several times, finds the causes in the compiler's own diagnostics and traces, and tries each fix it can apply without touching your files.
A fix is marked Verified only if it was applied, re-measured, came out faster than the run-to-run noise, and left the compiler's diagnostics byte-identical. Everything else is labelled for what it is.
| Evidence, not hints | Every finding lists the measured facts it comes from. Nothing is reported because a type merely looks big. |
| Fixes proven on your code | Fixes are applied in a temporary configuration, measured again, and the diagnostics compared byte for byte. |
| Honest numbers | Medians over several runs, with min, max and noise. A difference counts only when it is outside the noise. |
| TypeScript 5, 6 and 7 | Formats tested against real outputs of 5.9, 6.0 and 7.0. TypeScript 7 flags such as --checkers are detected, never assumed. |
| Built for CI | Compare a branch with main, set budgets, post one pull-request comment, read stable JSON (typescript-perf/v1). |
| Safe | Read-only on your project. Uses the compiler CLI only, never its API, so it measures exactly what your tsc does. |
- Quick start
- What you get: terminal, HTML report, JSON, Markdown
- Commands:
typescript-perf,why,compare,budget,ts7 - Findings and patterns
- Benchmarks
- How it works and what the results guarantee
- In CI: GitHub Action, other CI, budgets
- Reading the numbers
- Configuration · JSON and API · Compatibility
- Troubleshooting · FAQ
# 1. Measure, and get the verified fixes
npx typescript-perf
# 2. See where the check time goes: files, expressions, types, proven patterns
npx typescript-perf why
# 3. Open the full report in your browser
npx typescript-perf --htmlInstalled in the project, the shorter tsperf works too:
npm i -D typescript-perf # or: pnpm add -D typescript-perf / yarn add -D typescript-perf
npx tsperf whyRequires Node 20 or later and a project with TypeScript installed.
Each problem is presented the same way: Problem (what is going on), Evidence (the measured facts),
Impact (before/after, when the fix was re-measured), Solution (the change, as a diff), Status (Verified,
Suggested or Rejected, and why), Learn more (a page on how it is detected and fixed). Output adapts to the terminal
width, works without colors (NO_COLOR, CI logs), and links are clickable in terminals that support it.
--html writes a single, self-contained HTML file and opens it. Every page starts with a visual verdict:
| Command | First thing you see |
|---|---|
typescript-perf |
A gauge of the check time with its fast / moderate / slow zones, where the verified fix would bring it, and three counters: to fix, best verified gain, already fine |
why |
Your code vs libraries, and a treemap where each file's area is its check time (hot files in red) |
compare |
A full-width Failed, Slower or No regression banner, then each metric with its change |
ts7 |
The speed-up in large type, readiness, and a column chart of every variant |
budget |
Pass or fail, and how much of each budget is used |
Then each fix with its measured gain, the exact change as a diff, and its proof. What is already fine comes after, and the raw numbers last. No network access, light and dark themes, readable on a phone, printable. The JSON report is embedded and can be downloaded from the page.

--json: one object with a stable, documented schema (typescript-perf/v1, JSON Schema).--markdown: a pull-request comment (tables, collapsible details).--json-file <path>and--html-out <path>: several outputs from one run.
Measures the type-check (3 runs after a discarded warm-up run by default), then looks for known problems. Each finding that can be applied with a flag or a temporary tsconfig is re-measured before it is reported.
typescript-perf # tsconfig.json in the current directory
typescript-perf -p packages/web # another project
typescript-perf --runs 5 # tighter numbers
typescript-perf --no-findings # measure onlyOne compile with --generateTrace, streamed (traces of several GB are fine). It reports check time per file,
the slowest expressions, the types created per declaration (per checker on TypeScript 7), duplicate packages, and
the costly patterns the trace proves.
typescript-perf why
typescript-perf why --open # keep the trace and explore it in PerfettoChecks out <ref> in a temporary git worktree and measures both sides with interleaved runs, alternating the order,
so a slower machine affects both sides alike. Every delta comes with its noise.
typescript-perf compare origin/main
typescript-perf compare origin/main --fail-on instantiations:5%,checkTimeMs:15%
typescript-perf compare origin/main --why # which declarations created the extra typestypescript-perf budget --init # writes typescript-perf.config.ts from the current numbers, with a margin
typescript-perf budget # exit code 1 when over budgetYour current TypeScript against TypeScript 7 on this project: check time, single-threaded vs parallel, --checkers
values, memory, new or removed errors, and the tsconfig options each compiler rejects (from their own diagnostics).
typescript-perf ts7 # finds @typescript/native-preview or a "typescript@7" alias
typescript-perf ts7 --tsc7 path/to/tsc # or point to it| Option | |
|---|---|
-p, --project <path> |
tsconfig to measure (default tsconfig.json) |
-r, --runs <n> |
measured runs per side (default 3) |
--no-warmup |
keep the first run instead of discarding it |
--tsc <path> |
compiler to use (default: the project's own TypeScript) |
--tsc7 <path> |
TypeScript 7 compiler for ts7 |
--no-findings |
measure only |
--no-trace |
skip the trace: faster, but no barrel finding |
--why |
compare: attribute the change to declarations |
--fail-on <rules> |
compare: exit 1 above these growths, e.g. instantiations:5% |
--open |
why: keep the trace for Perfetto |
--html · --html-out <path> · --no-open |
HTML report |
--json · --json-file <path> |
JSON report |
--markdown |
Markdown (pull-request comment) |
Exit codes: 0 success · 1 a budget or --fail-on rule failed · 2 usage or compiler error.
| Finding | Detected from | Verified by re-measuring |
|---|---|---|
skip-lib-check |
a run with --skipLibCheck |
✔ |
stray-files |
--explainFiles: build output matched by include |
✔ with a temporary tsconfig |
checkers |
TypeScript 7 runs with --checkers 2 / 8 / 16 |
✔ |
barrel-imports |
--explainFiles and the check time in the trace |
the fix is in your imports |
duplicate-packages |
packageId in --explainFiles, @typescript/analyze-trace |
the fix is in your lockfile |
implicit-types |
--explainFiles, TypeScript 5 and earlier |
✔ with --types set to the packages the program uses |
explicit-return-types |
a declaration-only emit run | you choose the annotations |
why reports a pattern only when the trace shows its cost:
| Pattern | Proven by |
|---|---|
mapped-type-as-remap |
an as clause in the source, types created from it, and the check time of the files that use it |
recursive-conditional |
a self-referencing conditional type with ≥ 5% of all types, or depth-limit events on its types |
template-literal-union |
a union of ≥ 500 members, and ≥ 20 ms of relation checks on it |
variance |
the variance computation's own time (nested computations are not counted twice) |
barrel-reexports |
files only a barrel brings in, and their check time |
Real runs, not illustrations. Apple M3 Pro (12 CPUs, 36 GB), Node 22, 3 measured runs after a warm-up run, median.
| Project | Files | TypeScript 5/6 | TypeScript 7.0.2 | Single-threaded | Best --checkers |
New errors |
|---|---|---|---|---|---|---|
| Lab 001, typed events | 65 | 940 ms (6.0.3) | 340 ms (2.8×) | 469 ms | 341 ms (8) | none |
| Next.js 16 production app | 1,460 | 3.34 s (5.9.3) | 223 ms (15×) | 496 ms | 208 ms (8) | none |
| type-fest, full test suite | 653 | 79.2 s (6.0.3) | 14.7 s (5.4×) | 26.1 s | 9.7 s (8) | none |
On type-fest, TypeScript 6 needs a 16 GB heap (NODE_OPTIONS=--max-old-space-size=16384). Compiler-reported memory
went from 10.5 GB to 10.1 GB, and TypeScript 7's counters varied by less than 0.01% between runs.
Ratios depend on the machine's state. On the same laptop, another session measured the Next.js app at 4.97 s → 583 ms (8.5×) instead of 15×. Within a session the runs agree, but between sessions they can differ. Measure on your own CI runner before you plan around a number.
| Project | Result |
|---|---|
Lab 001 before/ |
why proves the as remap: 1,738 types created from SettingsEvents, 42% of the check time in the files that use it. skip-lib-check verified in every session, between −19% and −30% depending on the session, with identical diagnostics. |
Lab 001 after/ (fix applied) |
No pattern reported. |
| Next.js 16 app | Already well configured. One suggestion: 4 @types packages loaded but never used. |
| type-fest | No finding, by design: it disables skipLibCheck on purpose and ships its own .d.ts. why points to VariableLengthArraySliceHelper (6.1 s computing its variance) and the slowest type tests. |
The time of a few type-checks, plus one traced compile for findings and why:
| Project | typescript-perf |
why |
ts7 |
|---|---|---|---|
| Lab 001 (1 s check) | 23 s | 13 s | 13 s |
| Next.js app (3 s check) | 52 s | 16 s | 27 s |
| type-fest (80 s check) | 22 min | 15 min | 11 min |
flowchart LR
A[tsconfig] --> B["tsc --extendedDiagnostics<br/>× (1 warm-up + N runs)"]
B --> C[median · min · max · noise]
A --> D["tsc --explainFiles<br/>why each file is in"]
A --> E["tsc --generateTrace<br/>streamed"]
D --> F[candidate findings]
E --> F
F --> G{"apply the fix<br/>(flag or temporary tsconfig)"}
G --> H["measure again<br/>compare diagnostics"]
H --> I["Verified / Rejected"]
F --> J["Suggested<br/>(needs your decision)"]
- Find the compiler your project uses (
@typescript/native-preview,typescript, or an npm alias), and the flags it supports, from its own--help --all. - Measure. One discarded warm-up run, then N runs of
--extendedDiagnostics. Incremental projects run with--incremental false, so a build cache cannot fake a speed-up. Project references are built withtsc -b --forcein a temporary copy. - Explain.
--explainFilessays why every file is in the program. One--generateTracerun, streamed, gives time per file and expression, the types created and their declarations. - Verify. Fixes that need no decision from you are applied (a command-line flag or a temporary tsconfig that
extendsyours, in the system temp folder) and measured again. - Report, in the terminal, HTML, JSON or Markdown.
- Verified means: the fix was applied, every run with the fix was faster than every run without it, and the compiler's diagnostics were byte-identical. The JSON Schema rejects a verified finding without identical diagnostics.
- Suggested means the evidence is measured, but the fix needs a decision only you can make (how to split a barrel, which public type to declare, how to deduplicate a package).
- Already fine lists the checks that ran and passed, with what was measured. When a fix gives no gain outside the noise, it is reported there, not as a problem.
- Rejected means the fix was tried and would change your diagnostics: typescript-perf tells you how many errors would appear or disappear.
- A pattern in
whyis reported only with trace evidence of its cost. There is no "this type looks complex" heuristic. - Your files are never modified. The only file typescript-perf ever writes in your project is
typescript-perf.config.ts, and only onbudget --init.
# .github/workflows/typescript-perf.yml
name: typescript-perf
on: pull_request
permissions:
contents: read
pull-requests: write
jobs:
typescript-perf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
- run: npm ci
- uses: MartinFerret/typescript-perf@v0.2.0
with:
fail-on: instantiations:5%It compares the pull request with its base branch, checks the budget when typescript-perf.config.ts has one, keeps
one comment up to date on the pull request, and uploads the JSON report as an artifact.
| Input | Default | |
|---|---|---|
project |
tsconfig.json |
tsconfig, relative to working-directory |
working-directory |
. |
where node_modules and the tsconfig are |
runs |
3 |
runs per side |
base |
the PR's base branch | ref to compare with |
fail-on |
none | e.g. instantiations:5%,checkTimeMs:15% |
fail-on-budget |
true |
fail the job when the budget is exceeded |
comment |
true |
post or update the pull-request comment |
why |
false |
attribute instantiation changes to declarations (slower) |
npx typescript-perf compare origin/main --fail-on instantiations:5% --markdown --json-file typescript-perf.json
npx typescript-perf budget// typescript-perf.config.ts (written by `typescript-perf budget --init`)
import { defineConfig } from "typescript-perf";
export default defineConfig({
budget: {
instantiations: 1_250_000,
types: 410_000,
checkTimeMs: { max: 9000, tolerance: 10 },
},
});Gate on counters, not on time. Types and instantiations do not depend on the machine: on TypeScript 5/6 they are identical from run to run. On 7, parallel checking can make them vary slightly, so the default tolerance for counters is 1% there. Check time depends on the runner, so give it a generous margin or leave it out.
- Median, range and noise. Each number is the median of N runs after a discarded warm-up run. The range is the min and max over those runs. A difference counts only when it is larger than the range on both sides.
- Check time comes from the compiler's own
--extendedDiagnostics. TypeScript 5/6 print it in 10 ms steps. On TypeScript 7 it is wall time while several checkers run in parallel, so trace times, summed over checkers, can be larger than the check time. - Memory is what the compiler reports ("Memory used": the V8 heap on 5/6, the Go heap on 7). It is not the process RSS, and it is not comparable between compilers.
- Types and instantiations measure the type-level work. They are the most stable signal for CI.
- Several verified gains are each measured on their own. They overlap and do not add up.
Optional. typescript-perf.config.ts (or .mts, .mjs, .js, or a "typescript-perf" field in package.json):
import { defineConfig } from "typescript-perf";
export default defineConfig({
project: "tsconfig.app.json", // default tsconfig
runs: 5, // default number of runs
warmup: true, // discard a first run
budget: { instantiations: 1_250_000 },
});Command-line options take precedence.
import { analyze, why, compare, measure, checkBudget, ts7Report, renderHtml } from "typescript-perf";
const report = await analyze({ cwd: "packages/web", runs: 5 });
for (const f of report.findings ?? []) {
console.log(f.id, f.verification.status, f.evidence);
}Every function returns plain data typed by the package (Report, Finding, Measurement...), the same data as
--json. The format is documented in docs/schema.md and validated against the JSON Schema in the
test suite.
| Supported | Tested in CI | |
|---|---|---|
| TypeScript | 5.x, 6.0, 7.0 (typescript, @typescript/native-preview, npm aliases) |
5.9.3, 6.0.3, 7.0.2 |
| Node | 20 and later | 20, 22 |
| OS | Linux, macOS, Windows (best effort) | Ubuntu, macOS |
| Projects | single tsconfig, project references (tsc -b), incremental (Next.js), monorepo packages via -p |
fixtures for each |
"The compiler ran out of memory". Large type libraries need more than Node's default heap with TypeScript 5/6,
and your own tsc needs the same setting. typescript-perf passes NODE_OPTIONS to the compiler but never raises the
limit by itself, so it measures what your tsc does:
NODE_OPTIONS=--max-old-space-size=8192 npx typescript-perf"No TypeScript compiler found". If TypeScript is installed under another name (for example
"typescript6": "npm:typescript@6"), typescript-perf lists what it found and suggests a --tsc path.
"The compiler stopped on tsconfig errors". Both compilers still print statistics when the tsconfig has errors,
but they type-check nothing. typescript-perf says so instead of reporting meaningless numbers. Fix the listed options
first. typescript-perf ts7 shows which ones TypeScript 7 rejects.
Numbers vary a lot between runs. Close other heavy programs, plug in the laptop, and use --runs 5. Findings
that need a measured gain are not reported when the noise hides the gain: that is on purpose.
Why not just tsc --extendedDiagnostics or --generateTrace? That is where the numbers come from.
typescript-perf adds repeated runs and noise, finds the cause, tries the fix on your project, compares branches, and
turns a multi-GB trace into a list of declarations and patterns.
Does it use the TypeScript API? No. TypeScript 7 has no stable API yet, and typescript-perf must measure exactly
what your tsc does. Every format it reads is covered by tests on real outputs of 5.9, 6.0 and 7.0.
Is it safe to run on any project? It only runs the compiler with --noEmit (or emits declarations into a temporary
folder), and builds project references in a temporary copy. compare uses a temporary git worktree, removed at the
end.
A finding is wrong. Every finding lists the evidence it was built from. Please
open an issue with the --json output.
The patterns come from the TypeScript Performance Lab: measured, reproducible experiments on type-checking performance.
See CONTRIBUTING.md. Security issues: SECURITY.md.

