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:
- Rust compiler: template compilation moves entirely from the Go/JS era to Rust, greatly speeding up page rendering and builds.
- 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 rollupOptions → rolldownOptions
environments: {
client: {
build: {
// old: rollupOptions: { output: { manualChunks(id) { ... } } }
rolldownOptions: {
output: { /* ... */ }
}
}
}
}
2.2 manualChunks → codeSplitting.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-threebeing 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 likewebkitAudioContext,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.mjsmigration; - ~2.7x faster builds;
- The main cost is post-upgrade visual regression — especially the
compressHTMLwhitespace rule change. My advice is to keepcompressHTML: trueto 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.