Skip to content

About

Find out why TypeScript is slow on your project, with fixes proven on your own code. TypeScript 5, 6 and 7.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

typescript-perf

Find out why TypeScript is slow on your project, with fixes proven on your own code.

npm CI TypeScript 5 · 6 · 7 Node ≥ 20 License: MIT

Quick start · What you get · Commands · Benchmarks · How it works · CI · FAQ

typescript-perf output: measurement with median and range over 3 runs, then a finding with problem, evidence, before/after impact bars, the tsconfig change, and its verified status

npx typescript-perf

No 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.

Why typescript-perf

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.

Contents

Quick start

# 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 --html

Installed 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 why

Requires Node 20 or later and a project with TypeScript installed.

What you get

In the terminal

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.

typescript-perf why: check time per file with bars, slowest expressions, types created per declaration, and a proven costly pattern with its fix

A report you can share

--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.

HTML report: project and machine, key figures with the value of each run, summary of verified fixes, and finding cards with before/after bars and the configuration diff

For machines

  • --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.

Commands

typescript-perf: measure and fix

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 only

typescript-perf why: where the check time goes

One 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 Perfetto

typescript-perf compare <ref>: did this branch make it slower?

Checks 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 types

typescript-perf budget: limits for CI

typescript-perf budget --init       # writes typescript-perf.config.ts from the current numbers, with a margin
typescript-perf budget              # exit code 1 when over budget

typescript-perf ts7: should we move to TypeScript 7?

Your 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

Options

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.

Findings and patterns

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

Benchmarks

Real runs, not illustrations. Apple M3 Pro (12 CPUs, 36 GB), Node 22, 3 measured runs after a warm-up run, median.

Moving to TypeScript 7 (typescript-perf ts7)

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.

What it finds

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.

How long it takes

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

How it works

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)"]
Loading
  1. Find the compiler your project uses (@typescript/native-preview, typescript, or an npm alias), and the flags it supports, from its own --help --all.
  2. 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 with tsc -b --force in a temporary copy.
  3. Explain. --explainFiles says why every file is in the program. One --generateTrace run, streamed, gives time per file and expression, the types created and their declarations.
  4. Verify. Fixes that need no decision from you are applied (a command-line flag or a temporary tsconfig that extends yours, in the system temp folder) and measured again.
  5. Report, in the terminal, HTML, JSON or Markdown.

What the results guarantee

  • 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 why is 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 on budget --init.

In CI

GitHub Action

# .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)

Any other CI

npx typescript-perf compare origin/main --fail-on instantiations:5% --markdown --json-file typescript-perf.json
npx typescript-perf budget

Budgets

// 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.

Reading the numbers

  • 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.

Configuration

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.

JSON and API

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.

Compatibility

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

Troubleshooting

"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.

FAQ

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.

Background

The patterns come from the TypeScript Performance Lab: measured, reproducible experiments on type-checking performance.

Contributing

See CONTRIBUTING.md. Security issues: SECURITY.md.

License

MIT

About

Find out why TypeScript is slow on your project, with fixes proven on your own code. TypeScript 5, 6 and 7.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages