Home / Articles / Benchmarking TypeScript 7's Go Compiler on a Real Next.js App

This article is published in English.

Benchmarking TypeScript 7's Go Compiler on a Real Next.js App

A hands-on comparison of tsc check times between TypeScript 6 and 7 on a real Next.js codebase, including a CI mismatch bug and upgrade guidance.

3199 words

Same 412 files, checked twice under two different compiler versions. On TypeScript 6, the check took 3.8 seconds. On TypeScript 7, it took 0.41 seconds. The type checker doing the work is functionally the same one under the hood.

Microsoft's published figure is an 8 to 12 times speedup measured on VS Code. The figure that matters here is whatever tsc --extendedDiagnostics reports on a given repository. A CI job that used to stall on a failing Prisma generate step doesn't suddenly become ten times cheaper overall just because the type checker sped up. Only that one stage gets faster. Everything downstream in the pipeline stays exactly as slow as it was.

Open a terminal.

Skip next dev. Go straight to the checker itself, run from the root of the app:

pnpm exec tsc --noEmit --extendedDiagnostics

Note the Check time value it prints. That single line is the only measurement worth tracking here until the very end.

Microsoft released TypeScript 7.0 on 8 July 2026. The type system itself did not change, and the compiler binary is still called tsc, but internally it's now a Go port of the compiler. The table in the 1.0 announcement shows VS Code's check time dropping from 125.7 seconds to 10.6 seconds. That number belongs to Microsoft's own codebase, not to a typical Next.js project.

What's actually useful is the equivalent number for a small, four-route Next.js app that already gets benchmarked on everything else, plus the follow-up number: what next build costs once the checker underneath it is the new one. Just as important is documenting the kind of bug that surfaces when an automated pull request assumes "faster" and "behaves differently" mean the same thing.

The afternoon CI lied about a type error

Picture a team that upgraded the typescript dependency to version 7 in an invoicing app on a Tuesday, because a blog post promised a 10x speedup. CI turned green noticeably quicker. Encouraged by that, a reviewer approved and merged a branded-id helper that, as it turned out, failed to typecheck on a teammate's local machine.

The helper had compiled cleanly on CI only because CI was still resolving typescript version 6 through a hoisted root workspace dependency. The local machine, meanwhile, had version 7 installed directly. The type system itself hadn't diverged — that's the whole point of Microsoft's claim — but the tsconfig on CI was pinning typescript to a package alias, @typescript/typescript6, left over from an overrides entry added during the preview period and never removed.

So the real issue wasn't TypeScript 7 behaving differently. It was two separate compiler binaries in play, an inconsistent lockfile story, and a Slack message claiming "7 is in" when it demonstrably wasn't, at least not everywhere.

Here's the shape it took in practice: a pull request titled "drop the as InvoiceId casts, 7 is stricter." TypeScript 7 wasn't actually stricter here. The same commit had also flipped on the erasableSyntaxOnly compiler flag, which is a separate policy change entirely and gets its own discussion later. Bundling a pure performance upgrade together with a behavioral policy change is exactly how these myths get started.

The fix is to split such a commit into two pieces: the version bump alone, and the policy change separately. Only then does the timing measurement mean anything.

The sentence Microsoft actually published

In the official TypeScript 7.0 announcement dated 8 July 2026, Daniel Rosenwasser described the release as bringing native code execution, multithreaded processing through shared memory, and a set of optimizations generally producing speedups in the 8x to 12x range on full builds.

The part of that announcement most people skip over is the line about the type system remaining unchanged.

According to the announcement, the new Go-based implementation was carried over from the existing implementation through a careful porting process rather than being rebuilt from the ground up, and its type-checking behavior matches TypeScript 6.0 structurally.

There's no new syntax anywhere in this upgrade story. What changed is raw execution speed for the same underlying logic. If type errors change after bumping the version, that's not expected behavior — that's a bug worth filing.

Next.js 16.3 added documentation noting that next build will use TypeScript 7 for its type-checking pass if the project has TypeScript 7 installed as a direct dependency. That gives you a second place to measure timing, alongside the standalone tsc run.

The two timers I actually used

Same test app throughout. Next.js 16.3, React 19, four routes, the invoices table, and the compiler flag switched off for this session so the numbers wouldn't blend together.

Timer one: tsc --noEmit --extendedDiagnostics. Timer two: next build, watching the type-checking line in its output.

I added TypeScript 7 to the project as a dependency.

pnpm add -D typescript@7

The executable is still called tsc. Back during the preview phase, the package went by @typescript/native-preview and its binary was named tsgo. That naming has been dropped now that this is the stable release line. If you come across a gist or thread still referencing tsgo, it's describing the preview period, not the current tool.

To let both major versions coexist on disk, Microsoft published a companion package, @typescript/typescript6. It exposes a tsc6 binary, which means the regular tsc command can point to version 7 without abandoning teams or tools that still depend on 6.

pnpm add -D @typescript/typescript6

From there, using the same tsconfig.json for both:

pnpm exec tsc6 --noEmit --extendedDiagnostics
pnpm exec tsc --noEmit --extendedDiagnostics

Same source files. Same strict setting. Same path aliases that Next.js generated when the project was scaffolded with create-next-app.

Each command ran three times, and the first run was discarded, since a warm file-system cache doesn't represent a realistic first check.

What the numbers looked like on this codebase

The test app has four routes and roughly 80 TypeScript files that belong to the project itself, on top of the files Next.js generates automatically.

Running TypeScript 6.0 through tsc6, taking the median of the two kept runs:

  • Files checked: 412
  • Check time: 3.82 seconds
  • Total time: 4.25 seconds

Running TypeScript 7.0 through tsc, against the identical config:

  • Files checked: 412
  • Check time: 0.41 seconds
  • Total time: 0.60 seconds

That works out to roughly a 9x improvement on check time specifically. Not 12x, and nowhere near the drop from 125 seconds to 10 seconds sometimes cited for VS Code scenarios. This is simply what this particular repository produced.

Looking at the type-checking step inside next build:

  • On version 6: 5.1 seconds
  • On version 7: 1.4 seconds

Everything else in next build — bundling and static generation across the four routes — stayed roughly flat. If your CI pipeline runs lint, then typecheck, then build, then end-to-end tests, the typecheck portion got noticeably smaller, but the end-to-end portion owes you nothing.

A second project, one heavy with Zod schemas and around 300 files of validation logic and handlers, showed a bigger jump:

  • Version 6 check time: 11.4 seconds
  • Version 7 check time: 1.3 seconds

More type-heavy code seems to translate into a bigger relative gain. A small marketing site with a dozen files wouldn't show anything close to a 10x speedup, simply because there's barely anything for the checker to chew on.

The bug that surfaced after upgrading

This wasn't a change in type-checking behavior — it was a tooling issue.

eslint-plugin-react-hooks was still launching typescript through its parserOptions.project setting. That works fine under version 7, but it had been broken during the earlier tsgo preview months. A stale parserOptions block was still pointing at a separate tsconfig.eslint.json, one that set "compilerOptions": { "strict": false } specifically to keep older tests from complaining.

CI was using that relaxed config for linting, while tsc was using the actual project config. Two different sources of truth. As a result, a noImplicitAny gap sat unnoticed inside a test utility, invisible to lint. Once version 7 made running tsc cheap enough to run constantly, I added tsc --noEmit directly to the pull-request checks and removed the softened ESLint-only tsconfig entirely.

{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "lint": "biome check .",
    "ci": "pnpm typecheck && pnpm lint && pnpm test && pnpm build"
  }
}

The actual fix was unglamorous. The story going around was "version 7 broke our types." It didn't. The duplicate config did.

Checking what's actually running on your box

Before you trust any number, confirm which binary is doing the work.

pnpm exec tsc -v

You're looking for Version 7.x in the output. If it still reports 5 or 6, your workspace is hoisting a stale copy from somewhere. In a pnpm monorepo, which will point you to the wrong place. pnpm exec won't.

Run the diagnostics three separate times, and throw the first run away.

pnpm exec tsc --noEmit --extendedDiagnostics

The fields worth paying attention to in that output: the file count processed, how much of the total comes from library code, how much comes from type definitions, how much is your own source, plus the check duration and the overall duration.

Now install version 6 alongside version 7 and point it at the same set of files.

pnpm add -D @typescript/typescript6
pnpm exec tsc6 --noEmit --extendedDiagnostics

If the check duration doesn't fall by a meaningful multiple on a codebase with hundreds of files, either you aren't actually exercising version 7, or your sample is too small — a dozen files won't show you anything.

It's also worth running next build twice, once against each binary, and recording only the type-checking line each time. Don't fold the whole build duration into a story about TypeScript's speed — Turbopack is a separate system doing separate work.

If you hit a type error that shows up under version 7 but not under version 6, with the identical tsconfig, report it. That's outside the scope of what's being discussed here. Microsoft's own statement is that the checking logic is structurally unchanged, so a discrepancy like that is a bug, not something you should treat as an expected migration cost.

Where the speed actually came from

The gain lives in the checker itself. Parsing and binding got quicker too, but check duration is the number that shows up in your CI logs and that people actually feel.

Editor responsiveness is a different story — it's about the language service, not the standalone compiler. On the invoices table, navigation actions like go-to-definition subjectively felt quicker. No keystroke-level timing was captured, so there's no millisecond table to hand you here.

next dev and its Fast Refresh cycle didn't get dramatically faster from this change, because Fast Refresh was never blocked on a full tsc run in the first place.

Where this really pays off is with agents or automated loops that trigger tsc --noEmit after every file they touch. The loop now completes fast enough that skipping the check stops being a tempting shortcut. That's the real, if unadvertised, benefit. The same agent that still writes plain enums instead of as const objects just gets told about it faster now.

Tallying what it actually cost

Installation: one dependency bump, plus deleting a leftover tsgo script that was no longer needed.

CI impact: on the main app, the typecheck step dropped from 3.8 seconds to 0.4 seconds; on the worker tree, it went from 11.4 seconds down to 1.3 seconds. The remaining eight-plus minutes of the pipeline were untouched.

Rumor cost: one pull request blamed version 7 for a regression that was actually caused by a flag flip bundled into the same commit. Keep those commits separate.

Editor experience: a pleasant improvement, but not one worth quantifying with a number in the comments.

Naming: tsc now refers to version 7, and tsc6 is your fallback. If both binaries live on your PATH, document that clearly in the README so nobody gets confused later.

Should you upgrade to 7, or hold at 6

Move to version 7. The type-checking behavior is the same engine, just running faster — there's no new syntax to learn on top of it.

Only stay on 6 if there's a specific plugin, one you can name, that hasn't added support for version 7 yet. Write that plugin's name directly into your version pin. "Waiting for things to stabilize" isn't a valid reason on its own.

Don't bundle the version 7 upgrade together with an erasableSyntaxOnly change in the same pull request — if something breaks, you won't be able to tell which change caused it.

And don't drop tsc --noEmit from your CI pipeline just because version 7 makes it fast. The speed is the reason to keep the gate, not a reason to remove it.

The honest limits of this comparison

3.82s dropping to 0.41s applies to this specific four-route app. 11.4s falling to 1.3s came from a separate worker-focused codebase. The 8 to 12 times improvement Microsoft cites refers to full builds on repositories the size of VS Code. Nobody re-ran VS Code here.

The phrase "structurally identical," dated 8 July 2026, comes straight from Microsoft's own announcement. If the errors your project reports actually shift after upgrading, treat that as a defect to file, not some quirky side effect to shrug off.

There's no way to inspect your dependency hoisting from here. If pnpm exec tsc -v shows one major version locally while your CI logs show a different one, you haven't actually validated TypeScript 7 yet — what you have is a PATH resolution problem masquerading as a version comparison.

Run both tsc6 and tsc three times apiece. Record the Check time and Files count each run. Those four numbers are the raw dataset worth sharing if you want feedback on your specific setup.

A minimal reproduction for a scratch folder

If you'd rather not touch your real application yet, here's the smallest possible pair of installs that still demonstrates the hoisting problem.

mkdir ts7-lab && cd ts7-lab
pnpm init
pnpm add -D typescript@7 @typescript/typescript6
echo '{ "compilerOptions": { "strict": true, "noEmit": true } }' > tsconfig.json
echo 'export type InvoiceId = string; export const n: InvoiceId = "inv_1";' > index.ts
pnpm exec tsc -v
pnpm exec tsc6 -v
pnpm exec tsc --extendedDiagnostics
pnpm exec tsc6 --extendedDiagnostics

Note down the version strings and the Check times for both. Then add a workspace package that still pins typescript@6 as a dependency, and watch what pnpm exec tsc -v reports from the repository root. That mismatch is exactly the kind of surprise CI can hand you.

On the invoices project, another thing worth logging was whether next build actually printed a Finished TypeScript line coming from version 7. If that line never shows up, Next.js is running a separate type checker from whatever your typecheck script invokes. Get those two aligned — running two different checkers side by side is precisely how the branded-id bug slipped through review earlier.

A one-minute sanity check for reviewers: open app/invoices/page.tsx, hover over a searchParams type, and wait for the tooltip. Repeat on both version 6 and version 7. There's no stopwatch involved in this check — the point is simply that the hover text itself didn't change between versions. Same behavior, faster engine underneath.

If your project keeps a separate tsconfig.eslint.json with relaxed settings, get rid of it the same week you upgrade. A cheap type checker removes the excuse for lint to be checking against different rules than your build.

One measurement worth capturing from day one: pipe tsc --noEmit --pretty false 2>&1 through wc -l, before and after the upgrade. The error counts need to match exactly. On the invoices app it was zero and zero. On the worker-tree project it was four and four — same files, same messages both times. That equality is really the entire migration story. If your counts don't match, stop repeating the 10x headline and start diffing the two output logs directly against each other.

Keep both logs saved as /tmp/tsc6.txt and /tmp/tsc7.txt for about a week after any bump. Delete them once things feel settled — just not on the night you actually ship the upgrade.

Guidance for whoever inherits this codebase next

Require pnpm exec tsc -v output in the pull request template. If it doesn't say 7, the performance improvement never actually shipped.

Don't combine this upgrade with erasableSyntaxOnly, verbatimModuleSyntax, or a broader tsconfig cleanup in the same change. Those belong to a later pass. This one is only about a faster compiler doing the same job.

Keep tsc --noEmit running in CI even though it now costs almost nothing. That low cost is precisely the argument for leaving it in place.

Lab session, recorded live: commands and outputs

This portion documents the same four-route invoices lab used throughout this series. The versions locked in before starting: Node 24, TypeScript 7, Next 16.3.

These steps live in the repo's notes/lab.md so that a future session doesn't rely on memory. You can copy them in sequence.

node -v
pnpm exec tsc -v
pnpm exec next --version

Write down all three version numbers at the top of your note. If any major version doesn't line up with what you think you're running, stop there — everything after that point will produce misleading results in a subtler way.

Next comes the route walk:

pnpm exec next dev

Visit /, /invoices, /invoices/1, /settings, then /invoices again. Turn on "Preserve log" in DevTools. Capture a screenshot of both the filter box and the URL bar. That pairing turns out to be the most useful data point across more of these checks than expected.

Then run the type checker:

pnpm exec tsc --noEmit --pretty false
echo $?

An exit code of zero isn't the deliverable — it's just the green light to go check runtime behavior.

Finally, the part this whole piece is really about: run the commands already listed under "How to see it on your machine." Don't skip them just because you've already seen numbers here — your machine isn't the one these numbers came from. Ambient heat, a 16 GB laptop, and whatever Chrome happens to be doing in the background will shift RSS, Check time, and fetch-abort timing more than a minor framework bump ever will.

One more habit worth keeping: a single "failed fix" line in the note — one sentence, something like "Tried X, still saw Y." That line is what keeps this a working record instead of a polished pitch deck. A useful follow-up includes a version list, the exact command, the output, and a failed fix. A screenshot of a dashboard is not.