Skip to main content
Zhimalab
中文

Astro 6 → 7 Upgrade in Practice: Rust Compiler + Rolldown Migration Gotchas

2026-08-04 · 23 min

This site (zhimalab) used to run on Astro 6.4.8. When Astro 7 shipped stable — bringing a Rust compiler (a rewrite of @astrojs/compiler) and a built-in Vite 8 (the bundler switching from Rollup to Rolldown) — I upgraded the whole site in practice. This article records the complete process, the pitfalls hit, and the real gains.

Why the upgrade is worth it

Two big changes in Astro 7 are very friendly to static sites:

  1. Rust compiler: template compilation moves entirely from the Go/JS era to Rust, greatly speeding up page rendering and builds.
  2. Vite 8 + Rolldown: dependency pre-bundling and bundling all go through Rolldown — a faster build pipeline with more controllable chunk output.

For a pure static site like this one with 327 pages, the biggest direct benefit is build time.

Upgrade checklist

Only three dependencies changed; everything else (React 19, three, Tailwind, etc.) stayed untouched:

Dependency Old version New version
astro ^6.4.8 ^7.1.6
@astrojs/react ^5.0.7 ^6.0.2
@astrojs/mdx ^5.0.6 ^7.0.5
@astrojs/sitemap ^3.7.3 unchanged
pnpm install
pnpm astro build

Pitfall 1: the Rust compiler strictly validates HTML closing tags

The first build failed immediately with:

[astro] error   Closing tag has no matching opening tag

It traced to line 82 of src/pages/tutorial/geography-map.astro, which had a redundant </div>. Astro 6’s lenient compiler tolerated it, but the Rust compiler strictly validates tag pairing. After deleting it the build passed — out of 1000+ files in the whole repo, this was the only broken HTML, which was fairly lucky.

Pitfall 2: Vite 8 / Rolldown config migration

This was the most labor-intensive part of the upgrade. Our astro.config.mjs has fine-grained manual chunking (React, three, Phaser and MediaPipe each get their own chunk). Under Rolldown there are 3 things that must change:

2.1 rollupOptionsrolldownOptions

environments: {
  client: {
    build: {
      // old: rollupOptions: { output: { manualChunks(id) { ... } } }
      rolldownOptions: {
        output: { /* ... */ }
      }
    }
  }
}

2.2 manualChunkscodeSplitting.groups (the important one)

output.manualChunks is deprecated in Rolldown, replaced by output.codeSplitting.groups. This is the biggest trap: Rolldown’s groups capture dependencies recursively, and which group captures first is decided by priority. Without priorities, the @react-three group would swallow react and three into itself first, merging React core + three.js + the whole R3F ecosystem into one 1.3MB giant chunk that every page is forced to load.

The correct approach is to explicitly set priorities on the React/three-related groups:

codeSplitting: {
  groups: [
    { test: /node_modules[\\/](react|react-dom|scheduler)[\\/]/, name: 'react-vendor', priority: 30 },
    { test: /node_modules[\\/]three[\\/]/, name: 'three-core', priority: 20 },
    { test: /node_modules[\\/]@react-three[\\/]/, name: 'react-three', priority: 10 },
    { test: /node_modules[\\/]@mediapipe[\\/]/, name: 'mediapipe' },
    { test: /node_modules[\\/]phaser[\\/]/, name: 'phaser' },
    { test: /node_modules[\\/]html2canvas[\\/]/, name: 'html2canvas' },
    { test: /node_modules[\\/]prismjs[\\/]/, name: 'prism' },
    { test: /node_modules[\\/]lucide-react[\\/]/, name: 'lucide' },
  ],
}

Two small details:

  • Use [\\/] in the regex to be compatible with the Windows path separator (official recommendation).
  • The react group should be written as (react|react-dom|scheduler)[\\/] to avoid the react inside @react-three being matched into react-vendor.

After migration, the measured chunk split is exactly the same as Astro 6: react-vendor 186KB, three-core 694KB, react-three 451KB, phaser 1343KB, html2canvas 195KB, mediapipe 133KB. Ordinary tool pages only load react-vendor + rolldown-runtime, unaffected by the big chunks.

2.3 the location of optimizeDeps define changed

The old config used optimizeDeps.esbuildOptions.define to pin React dev mode’s NODE_ENV, which errors directly under Vite 8:

Invalid key: Expected never but received define

In Rolldown, define moved to the transform level:

optimizeDeps: {
  rolldownOptions: {
    transform: {
      define: { 'process.env.NODE_ENV': '"development"' },
    },
  },
}

Pitfall 3: a visible regression from JSX whitespace rules

This is the sneakiest pitfall. Astro 7’s compressHTML default changed from “HTML semantic compression” to 'jsx' (JSX rules):

  • Whitespace on the same line is preserved;
  • whitespace across lines / at line breaks is removed.

For a Chinese-content site like this one, the impact is visible. The build output directly reproduced text sticking together on the 404 page:

<!-- source template -->
<span>🫘</span>
芝麻开门 —— 但这次门后面什么也没有

<!-- Astro 7 default 'jsx' mode output -->
<span>🫘</span>芝麻开门 —— 但这次门后面什么也没有

Scanning the whole repo found ~70 files with “inline element spanning lines + text” patterns (the home page’s ⚡ 快捷体验传送门, the poem game’s 💡 小锦囊, etc. — about 50 spots with real visible risk). Adding {" "} manually one by one is impractical; the officially suggested fix is to set it explicitly in astro.config.mjs:

export default defineConfig({
  compressHTML: true, // restore Astro 6's HTML semantic compression
})

After this change and a rebuild, whitespace returned to normal and all 327 pages passed at once. If you don’t handle this after upgrading, it silently becomes a visual regression.

The payoff: 2.7x faster builds

Stage Astro 6.4 Astro 7.1 Improvement
Build core 1m 21s 29s ~2.8x
Full site (327 pages) 1m 31s 37s ~2.5x

In practice, the whole build went 1m31s → 37s, roughly a 2.7x speedup. For CI deployments that is a very meaningful gain.

Verification

After upgrading, the following checks all passed:

  • pnpm astro build: all 327 pages built successfully, sitemap generated normally.
  • Dev server: sampled 5 pages, all 200, clean logs.
  • Production preview + Playwright smoke: /, /tools/sums-out, /ai, /tutorial/geography-map, /blog — 5/5 passed, React islands hydrated correctly, no console errors.
  • pnpm generate:index: search index generated normally (103 documents).

Remaining risks and pre-existing issues

A few points to watch after the upgrade, some unrelated to it:

  • Raised browser targets: Vite 8 / Rolldown default output targets now target Chrome 111+ / Firefox 114+ / Safari 16.4+. Fine for modern browsers; configure a target if you need old-browser support.
  • CJS interop: Rolldown’s interop rules for CommonJS modules differ from Rollup; a few old packages may behave differently. This project saw no issues in build + smoke tests.
  • astro check’s 7000+ errors are pre-existing: mainly pure-JS DOM type errors like webkitAudioContext, execCommand, present under Astro 6 as well and unrelated to the upgrade.
  • robots.txt has always been missing: the repo never had a public/robots.txt, so assertions on it in Playwright cases 404 — another pre-existing issue; adding one is a quick fix.

Summary

The Astro 6 → 7 upgrade was low-risk, high-reward for this site:

  • Minimal code changes: 3 dependency version bumps + 1 broken-HTML fix + the astro.config.mjs migration;
  • ~2.7x faster builds;
  • The main cost is post-upgrade visual regression — especially the compressHTML whitespace rule change. My advice is to keep compressHTML: true to preserve existing rendering behavior after upgrading, then run a full-site screenshot comparison.

If you’re considering the upgrade too, focus on three things: the Rust compiler’s tag-closing validation, the priority of Rolldown manual groups, and compressHTML’s whitespace rules. Get past these three, and the rest is mostly grunt work.