# Faster builds: migrating to the TypeScript 7 native compiler and Rolldown

> Migration strategies for TypeScript 7 and Rolldown in a monorepo: default breaking changes, --checkers and --builders, Vite 8, tsdown, and reliable CI/CD benchmarks.

- Autore / Author: Diego Betto (https://diegobetto.com/about/)
- Pubblicato / Published: 2026-10-09
- URL: https://diegobetto.com/en/faster-builds-typescript-7-rolldown/
- Tag: typescript, javascript, vite, rolldown, ci-cd

For a decade, the JavaScript toolchain was mostly written in JavaScript. It worked, but on a monorepo with a few hundred thousand lines, type-checking in CI took minutes and the production build took just as long. In 2026 the two slowest pieces of the pipeline were replaced by native engines: the **TypeScript 7 compiler**, ported to Go, and **Rolldown**, the Rust bundler that's the default since Vite 8. Both promise builds 8 to 10 times faster, and sometimes more.

Promises, however, need measuring. In this article we look at how to migrate a monorepo incrementally: how to handle the default breaking changes, how to configure parallelism with `--checkers` and `--builders` without saturating your runners, and how to build a CI benchmark that actually tells you how much time and memory you saved.

### First: split type-checking from building

The biggest win doesn't come from a tool but from an architectural decision many projects have already half made: **whatever checks the types isn't what produces the JavaScript**.

| Task                            | Tool in 2026                          | What it does                                         |
| ------------------------------- | ------------------------------------- | ---------------------------------------------------- |
| Type-checking                   | `tsc` (TypeScript 7, Go)              | Checks types, `--noEmit`                             |
| App transpiling and bundling    | Vite 8 with Rolldown + Oxc (Rust)     | Strips types file by file, bundles, minifies         |
| Library builds + `.d.ts`        | tsdown (Rolldown) or `tsc --build`    | Package JavaScript and type declarations             |

The bundler doesn't read types: Oxc strips them file by file, without knowing about the rest of the program. That's why the two steps can run **in parallel** as separate jobs, and a type error doesn't have to block build feedback (or vice versa). If your build script today is `tsc && vite build`, this is the first change to make, before even bumping versions.

```json
// an app's package.json
{
  "scripts": {
    "typecheck": "tsc --noEmit --checkers 4",
    "build": "vite build"
  }
}
```

### Step 0: measure your starting point

Without a baseline you'll never know whether the migration paid off, or what to blame for a regression. Measure three things, separately, on the same machine:

- **type-check time**, cold;
- **build time**, cold (no Turborepo/Nx cache, no `node_modules/.vite`);
- **peak memory** for each process.

[hyperfine](https://github.com/sharkdp/hyperfine) is the right tool for timings, because it repeats the command and reports mean and standard deviation:

```bash
hyperfine --warmup 1 --runs 5 \
  --prepare 'rm -rf dist node_modules/.vite' \
  'npx vite build'
```

For memory, `/usr/bin/time -v` (GNU time, on Linux) reports the _Maximum resident set size_:

```bash
/usr/bin/time -v npx tsc --noEmit 2>&1 | grep "Maximum resident"
```

> **Measure cold and warm**
>
> In a monorepo with a remote cache (Turborepo, Nx), most CI builds are "warm": untouched packages
> are restored from cache. Native tools pay off mostly on cache misses — a PR touching a package at
> the bottom of the graph, or the first build on a new branch. Measure both cases, or you risk
> underestimating (or overestimating) the result.

### Part 1: TypeScript 7

TypeScript 7 is a **port**, not a rewrite: same checker, same type semantics as 6.0. What changes are the `tsconfig.json` defaults, the removed options, and parallelism. The details are in the [TypeScript 7 deep dive](https://diegobetto.com/en/typescript-7-deep-dive/); here we focus on applying them to a monorepo without stopping the team.

#### Strategy: 6.0 first, then 7.0

TypeScript 6.0 exists precisely as a bridge: it flags as deprecations everything that becomes an error in 7.0. The safest sequence is:

1. upgrade the whole monorepo to **6.0** and clear every deprecation warning;
2. turn on, in 6.0, the options that become defaults in 7.0 (`stableTypeOrdering`, explicit `types`, explicit `rootDir`);
3. only then move to **7.0**, which by that point should need few changes.

#### Default breaking changes in a monorepo

In a single project the new defaults take ten minutes to fix. In a monorepo the problem is that every package has its own `tsconfig.json`, often extending a shared base. It pays to pin everything **in the base**, explicitly, so the migration becomes a one-file change:

```json
// tsconfig.base.json
{
  "compilerOptions": {
    "strict": true,
    "target": "es2024",
    "module": "esnext",
    "moduleResolution": "bundler",
    "types": [],
    "noUncheckedSideEffectImports": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true
  }
}
```

```json
// packages/api/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist",
    "types": ["node"]
  },
  "include": ["src"]
}
```

The three things that break most often:

- **`types: []`**: Node packages lose `process` and `Buffer`, test packages lose `describe` and `expect`. Each package declares its own globals.
- **`rootDir`**: where it isn't explicit, the `dist/` layout can change (`dist/src/index.js` instead of `dist/index.js`) and break the `exports` in `package.json`. Check it package by package.
- **`baseUrl` removed**: aliases must be rewritten as `paths` relative to the `tsconfig.json` (`"@/*": ["./src/*"]`).

`isolatedModules` and `verbatimModuleSyntax` aren't required by TypeScript 7, but they guarantee every file can be transpiled on its own — meaning Oxc and Rolldown produce the same output as `tsc`. If you split type-checking from building, turn them on.

#### The bridge for tooling: TypeScript 6 alongside

TypeScript 7.0 doesn't have a stable programmatic API yet: typescript-eslint, Volar (Vue), and the Svelte, Astro, and MDX plugins still import `typescript` and need 6.0 until 7.1. The fix is to keep both compilers, with an npm alias for the 6.0 package (`@typescript/typescript6`) as described in the [deep dive](https://diegobetto.com/en/typescript-7-deep-dive/): the linter uses 6.0, CI type-checking uses the native binary.

Having both installed has a side benefit: you can compare results. Before turning 6.0 off in CI, run both for a few days and diff the diagnostics:

```bash
npx tsc6 -p packages/api --noEmit --pretty false | sort > ts6.txt
npx tsc  -p packages/api --noEmit --pretty false --checkers 4 | sort > ts7.txt
diff ts6.txt ts7.txt
```

Sorting matters because diagnostic order can change with parallelism. The expected differences come from the new defaults and stable type ordering in messages (`string | number` vs `number | string`); anything else should be understood before you trust the new compiler.

#### `--checkers` and `--builders` without saturating the CPU

TypeScript 7 introduces two levels of parallelism:

- **`--checkers N`** (default 4): how many type checkers work in parallel on files of the _same_ project.
- **`--builders N`**: how many referenced projects are built at once with `tsc --build`.

What synthetic benchmarks don't show is that in a monorepo these two numbers **multiply**, and they multiply with your orchestrator's parallelism too:

```
type-check threads ≈ parallel Turborepo/Nx tasks × builders × checkers
```

Turborepo runs up to 10 tasks in parallel by default. Ten packages each running `tsc --checkers 4` means 40 checkers on a standard GitHub Actions runner with 4 vCPUs and 16 GB of RAM: the result isn't faster, it's slower and at risk of OOM, because each checker builds its own types and memory grows with the checker count.

Two strategies work:

**A. A single `tsc --build` from the root**, letting TypeScript manage the graph:

```json
// tsconfig.json at the monorepo root
{
  "files": [],
  "references": [
    { "path": "packages/core" },
    { "path": "packages/ui" },
    { "path": "packages/api" },
    { "path": "apps/web" }
  ]
}
```

```bash
npx tsc --build --builders 2 --checkers 2
```

With project references, packages need `composite: true` and must emit declarations (`emitDeclarationOnly`), but you get `tsc --build`'s incrementality: unchanged projects aren't re-checked.

**B. The orchestrator owns parallelism**, and each `tsc` uses little of it:

```bash
turbo run typecheck --concurrency=2
# with "typecheck": "tsc --noEmit --checkers 2" in every package
```

This strategy plays better with Turborepo/Nx remote caching, because each package is a separate cacheable task.

Either way the rule is the same: **the product of the parallelism levels shouldn't go far beyond the available cores**. And pin the values in your scripts instead of relying on defaults: beyond performance, a different checker count can, in rare cases, produce different diagnostics, so local and CI must use the same values.

> **GOMAXPROCS**
>
> The native compiler is a Go program, so it honors the `GOMAXPROCS` environment variable, which caps
> the number of OS threads executing Go code at once. Recent Go runtimes already read container CPU
> limits (cgroups), but on shared runners or with unusual limits, setting it explicitly is a simple
> way to cap the CPU usage of every `tsc` process in a job.

#### `.d.ts` files without a type checker: `isolatedDeclarations`

In monorepos with many internal libraries, generating `.d.ts` files is often the slowest part of the build, because it needs the type checker. With `isolatedDeclarations: true`, TypeScript requires you to explicitly annotate exported types (return types of public functions, exported constants):

```ts
// ❌ with isolatedDeclarations: the return type must be inferred from the body
export function createClient(url: string) {
  return new ApiClient(url);
}

// ✅ the type is declared, the .d.ts can be generated from this file alone
export function createClient(url: string): ApiClient {
  return new ApiClient(url);
}
```

In exchange, declarations can be generated file by file, without a type checker, using Oxc. That's what **tsdown** does — the Rolldown-based library bundler and the natural successor to tsup:

```ts
// packages/core/tsdown.config.ts
import { defineConfig } from "tsdown";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm"],
  dts: true, // with isolatedDeclarations on, uses Oxc instead of the compiler
});
```

Type-checking stays `tsc --noEmit`'s job; the library build becomes independent of the checker and much faster.

### Part 2: Rolldown with Vite 8

Vite 8, stable since March 2026, replaces the esbuild (dev) + Rollup (build) pair with a single bundler, **Rolldown**, which reached 1.0 stable in May 2026. JS/TS transforms and minification move to **Oxc**, and CSS is minified with Lightning CSS. The benefit isn't only speed: dev and build finally use the same engine, so a whole class of "works in dev, breaks in build" bugs goes away.

#### A two-step migration

For anything beyond a trivial project, separate the bundler switch from the Vite version bump:

**1. On Vite 7, try `rolldown-vite`** (the package that previewed the integration) as a drop-in:

```json
{
  "devDependencies": {
    "vite": "npm:rolldown-vite@7.2.2"
  }
}
```

If something breaks here, you know it's Rolldown and not some other Vite 8 change.

**2. Move to Vite 8** by replacing the alias with `"vite": "^8.0.0"` and applying the config renames.

#### The main configuration changes

Vite 8 includes a compatibility layer that automatically converts much of the old configuration, but it's worth migrating explicitly so you don't depend on deprecated options:

| Vite 7                                   | Vite 8                                              |
| ---------------------------------------- | --------------------------------------------------- |
| `build.rollupOptions`                    | `build.rolldownOptions`                             |
| `worker.rollupOptions`                   | `worker.rolldownOptions`                            |
| `optimizeDeps.esbuildOptions`            | `optimizeDeps.rolldownOptions`                      |
| `esbuild` (JS/TS transform)              | `oxc`                                               |
| `esbuild.jsx: "automatic"`               | `oxc.jsx: { runtime: "automatic" }`                 |
| `output.manualChunks` (object)           | removed → `output.codeSplitting`                    |
| `output.manualChunks` (function)         | deprecated → `output.codeSplitting`                 |
| JS minification with esbuild             | Oxc (default)                                       |
| CSS minification with esbuild            | Lightning CSS (default)                             |
| `transformWithEsbuild` (plugins)         | `transformWithOxc`                                  |

The most common case in a real app is vendor splitting. Before:

```ts
// vite.config.ts — Vite 7
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          react: ["react", "react-dom"],
          charts: ["recharts"],
        },
      },
    },
  },
});
```

After, with `codeSplitting` groups, which support priorities and size thresholds:

```ts
// vite.config.ts — Vite 8
export default defineConfig({
  build: {
    rolldownOptions: {
      output: {
        codeSplitting: {
          groups: [
            { name: "react", test: /node_modules[\\/]react(-dom)?[\\/]/, priority: 20 },
            { name: "charts", test: /node_modules[\\/]recharts/, priority: 15 },
            { name: "vendor", test: /node_modules/, priority: 10 },
          ],
        },
      },
    },
  },
});
```

#### What can break

- **CommonJS interop**: the `default` import from CJS modules is now handled consistently, but differently from before. If `import x from "some-cjs-package"` returns the wrong object, `legacy.inconsistentCjsInterop: true` temporarily restores the old behavior while you fix the code.
- **Plugins relying on internals**: plugins using unsupported Rollup hooks (`resolveImportMeta`, `renderDynamicImport`, `resolveFileUrl`, `shouldTransformCachedModule`) or calling esbuild directly need checking one by one. esbuild is no longer a Vite dependency: if a plugin needs it, install it as a devDependency.
- **Decorators**: Oxc doesn't lower native decorators. If you use them (or legacy decorators with `experimentalDecorators`), you need `@rolldown/plugin-babel` or `@rollup/plugin-swc`.
- **Browser target**: the default rises to Chrome/Edge 111, Firefox 114, Safari 16.4. If you support older browsers, set `build.target` explicitly.
- **Output formats**: Rolldown doesn't support `system` or `amd`.
- **esbuild options without an equivalent**: `esbuild.banner`/`footer` need a small custom plugin; `mangleProps` and friends aren't supported.

> **Check the bundle, not just the build**
>
> A build that finishes without errors doesn't mean an equivalent bundle. After migrating, compare
> chunk count and size (for example with `rollup-plugin-visualizer`, which works with Rolldown), run
> end-to-end tests against the production build, and keep an eye on
> [Core Web Vitals](https://diegobetto.com/en/core-web-vitals-lcp-cls-inp/): a vendor chunk split differently
> can move LCP more than the faster build saves you in CI.

### Part 3: a benchmark that holds up in CI

Release-post benchmarks run on very large codebases and powerful machines. What matters is your monorepo on your runner. A minimal GitHub Actions workflow, triggered manually on the migration branch:

```yaml
# .github/workflows/build-benchmark.yml
name: build-benchmark
on: workflow_dispatch

jobs:
  bench:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v5
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: sudo apt-get update && sudo apt-get install -y hyperfine

      - name: Type-check (TS 6 vs TS 7)
        run: |
          hyperfine --warmup 1 --runs 5 --export-markdown typecheck.md \
            'npx tsc6 -b --force' \
            'npx tsc -b --force --builders 2 --checkers 2'

      - name: Peak memory
        run: |
          /usr/bin/time -v npx tsc6 -b --force 2>&1 | grep "Maximum resident" | tee -a memory.txt
          /usr/bin/time -v npx tsc  -b --force --builders 2 --checkers 2 2>&1 | grep "Maximum resident" | tee -a memory.txt

      - name: Build app (cold)
        run: |
          hyperfine --runs 3 --prepare 'rm -rf apps/web/dist apps/web/node_modules/.vite' \
            --export-markdown build.md 'npx turbo run build --filter=web --force'

      - run: cat typecheck.md build.md memory.txt >> "$GITHUB_STEP_SUMMARY"
```

A few tips for numbers you can trust:

- **Same runner, same job.** Shared runners are noisy: comparing yesterday's job with today's tells you little. Run baseline and new version in the same job, back to back.
- **`--force`** on `tsc -b` and on Turborepo/Nx, to rule out incremental and remote caches.
- **Repeat.** Five hyperfine runs and the standard deviation tell you whether a 10% difference is real or noise.
- **Memory, not just time.** Raising `--checkers` cuts time and increases RAM. On small runners the sweet spot is often below the default.
- **Measure total pipeline time**, not just individual commands: if type-checking and building now run in parallel, the real gain is the longest job, not the sum.
- **Try several parallelism values** on your typical machine (say `--checkers 1, 2, 4, 8`) before picking one: the curve flattens early, and past a certain point extra checkers only cost memory.

One last note on numbers: if `tsc` used to take 40 seconds and now takes 5, but the whole pipeline goes from 9 to 7 minutes, the bottleneck was elsewhere (dependency installs, tests, artifact uploads). It's still a good result, but that's the number to share with your team.

### Migration checklist

1. Split type-checking (`tsc --noEmit`) and building (bundler) into separate scripts and jobs.
2. Measure the baseline: cold and warm timings, peak memory.
3. Upgrade to TypeScript 6.0 and clear all deprecations.
4. Make `types`, `rootDir`, `module`, and `moduleResolution` explicit in `tsconfig.base.json`; remove `baseUrl`.
5. Install TypeScript 7 while keeping `@typescript/typescript6` for the linter and plugins; diff the two compilers' diagnostics.
6. Pick a parallelism strategy (a single `tsc --build` or the orchestrator) and pin `--checkers`/`--builders` in your scripts.
7. Evaluate `isolatedDeclarations` and tsdown for internal libraries.
8. Try `rolldown-vite` on Vite 7, then move to Vite 8 and rename options (`rolldownOptions`, `oxc`, `codeSplitting`).
9. Check plugins, CJS interop, decorators, and browser target.
10. Compare bundles and run end-to-end tests against the production build.
11. Repeat the benchmark in the same CI job and document time, memory, and total pipeline duration.

### FAQ

> **Should I migrate TypeScript and Vite together?**
>
> No, and it's better not to. They're two independent migrations: TypeScript 7 changes
> type-checking, Rolldown changes bundling. Doing them in separate PRs makes every regression
> attributable to a single cause.

> **How many checkers should I use in CI?**
>
> It depends on the runner and on how many tasks run in parallel. On a 4-vCPU runner with a single
> `tsc --build`, the default of 4 is reasonable; if the orchestrator already runs several tasks at
> once, drop to 1–2 checkers per process. Measure time and memory with two or three values and pin
> the best one.

> **Can I use TypeScript 7 with typescript-eslint or Vue?**
>
> Yes for CI type-checking, not (yet) for tools that use the compiler API: until TypeScript 7.1, the
> linter, Volar, and framework plugins need 6.0 through the `@typescript/typescript6` package.

> **Does Vite 8 type-check my code?**
>
> No, just like previous versions: Oxc strips types without checking them. Type-checking runs with
> `tsc --noEmit` in a separate script or job (or with `vite-plugin-checker` if you want it during
> development).

> **Does Rolldown work without Vite?**
>
> Yes. Rolldown has its own CLI and a Rollup-compatible API, and it powers tsdown for libraries. For
> applications, though, Vite 8 remains the easiest way to adopt it.

### Conclusion

Migrating to native tools is one of the few optimizations that doesn't ask you to rewrite application code: the language stays the same, the bundle should stay equivalent, only the engines change. The real work is at the edges — config defaults, plugins, parallelism that multiplies across layers — and that's where an incremental migration and an honest benchmark make the difference between "we bumped our dependencies" and "CI now takes half the time".

If you want to start from what's new in the compiler, read the [introduction to TypeScript 7](https://diegobetto.com/en/typescript-7-whats-new/); for details on every removed option, see the [deep dive](https://diegobetto.com/en/typescript-7-deep-dive/).

**References**

- [TypeScript Blog – TypeScript 7.0 announcements](https://devblogs.microsoft.com/typescript/)
- [TypeScript 7.1 Iteration Plan](https://github.com/microsoft/TypeScript/issues/63703)
- [Vite – Migration from v7](https://vite.dev/guide/migration)
- [Rolldown – codeSplitting option](https://rolldown.rs/reference/OutputOptions.codeSplitting)
- [tsdown – the Rolldown-based library bundler](https://tsdown.dev/)
- [TSConfig – isolatedDeclarations](https://www.typescriptlang.org/tsconfig/#isolatedDeclarations)
