
最近我把博客从 Astro 6 升级到了 Astro 7。部署完成后,最直观的感受不是某个新 API,也不是构建日志里的版本号,而是:页面像是突然“秒开”了。
特别是 About 页面。以前进入页面时,总能明显看到一段 Loading;升级后,这个等待几乎一闪而过。
一开始我也怀疑这只是缓存、网络状态或者刚部署后的心理作用。于是我保留了升级前后的两个不可变 Vercel Deployment,从服务端响应、静态资源和客户端依赖链三个层面做了一次对比。
最后发现,真正影响体感的并不是“Astro 7 天生更快”,而是升级过程中发生的一次分包策略重构:旧配置把本不属于 About 页的大量 Three.js 生态代码塞进了它的首屏依赖,新配置则按照入口重新隔离了这些模块。
先说结论
下面的体积来自 Vercel 上两个不可变生产部署的已最小化(minified)代码产物,但没有启用 HTTP gzip/Brotli,因此更接近浏览器解压后需要解析的体积。实际网络传输会更小,不过 JavaScript 下载后的解析和执行负担仍然存在。

其中 statue.glb 始终是 2.23 MB,真正消失的是模型请求之前多余的 JavaScript 等待。
我是怎么对比两个版本的
这次分析没有拿本地开发服务器和线上环境比较,而是选择了升级前后的两个不可变 Vercel Deployment。这样可以保证每次请求对应的代码不会发生变化。
先用 inspect 确认提交、运行时、区域和 Function 产物:
vercel inspect <deployment-url> –json
再通过已认证的 vercel curl 绕过 Deployment Protection,检查缓存状态和响应大小:
vercel curl / \\
–deployment <deployment-url> \\
— –silent –output /dev/null –dump-header –
vercel curl /_astro/<chunk>.js \\
–deployment <deployment-url> \\
— –silent –output /dev/null \\
–write-out 'bytes=%{size_download}\\n'
最后从 HTML 中找到 component-url、renderer-url 和入口脚本,再继续读取每个脚本的静态 import。这里不能只统计 HTML 直接引用的文件,否则会漏掉 react-vendor、three-vendor 等真正占体积的同步依赖。
第一步:先排除几个看起来最像答案的因素
性能问题最容易被一个醒目的版本号带偏。既然刚升级 Astro 7,第一反应自然是“新版本运行时更快了”。但实际对比后,有几个因素可以先排除。
不是 Node.js 22
升级后的 Vercel Function 使用 Node.js 22,但升级前的部署同样已经运行在 Node.js 22。Node 版本没有变化,因此不能解释这次体感差异。
不是 HTML 突然变小
两版首页 HTML 分别是 107,960 Bytes 和 107,978 Bytes,几乎完全一致。虽然升级配置中显式启用了 compressHTML: true,但它并没有让这个页面的响应体出现数量级变化。
不是 CSS 大幅减少
首页关键 CSS 从 222,454 Bytes 变成 221,084 Bytes,只减少了约 1.4 KB。这个变化当然是正向的,但不足以解释 Loading 明显缩短。
也不能只归因于 Vercel 缓存
两版页面都由香港区域返回,响应头都是 x-vercel-cache: HIT。缓存命中后的常见 TTFB 都落在约 0.12~0.16 秒,区间高度重叠。
也就是说,服务端已经很快了。真正的差异更可能发生在浏览器拿到 HTML 之后。
第二步:从 HTML 继续追客户端依赖
这个博客大部分内容由 Astro 在服务端输出,但 Header 里的搜索、主题切换和 Orama 搜索使用了客户端 island:
<OramaBox client:only='react' />
<Search client:only='react' />
<ToggleTheme client:load />
client:only 和 client:load 都意味着相关模块会立即进入客户端加载链。它们不一定阻塞静态正文的首次绘制,却会影响搜索框、主题切换等区域什么时候真正出现并可交互。
对首页所有入口脚本和同步依赖去重后,JavaScript 总量从约 433 KB 降到了 219 KB,接近减少一半。
这已经足以改善普通页面的完成感,但 About 页的变化更加明显。
第三步:About 页为什么真的会显示 Loading
About 页不是纯静态页面,而是一个 client:only='react' 的 React 应用。它启动后通过 GLTFLoader 请求一个 3D 模型:
const [statueMesh, setStatueMesh] = useState(null);
useEffect(() => {
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/gltf/');
loader.setDRACOLoader(dracoLoader);
loader.load('/statue.glb', (gltf) => {
setStatueMesh(gltf.scene);
});
}, []);
模型没有加载完成时,组件会展示一个占满视口的 Loading SVG:
return statueMesh ? (
<Suspense>{/* About 页面内容 */}</Suspense>
) : (
<div className='flex h-screen w-full items-center justify-center'>
<LoadingSVG />
</div>
);
模型请求要等 About 入口及同步依赖下载、解析,直到 React 执行 useEffect 后才会发出。因此,阻塞初始化的大型 JavaScript 会把整条加载链一起向后推。
根因:一个过于宽泛的 manualChunks
升级前,我使用下面的规则拆分第三方依赖:
manualChunks: (id) => {
if (id.includes('node_modules/react') || id.includes('node_modules/react-dom')) {
return 'react-vendor';
}
if (id.includes('node_modules/three') || id.includes('@react-three')) {
return 'three-vendor';
}
if (id.includes('framer-motion') || id.includes('motion')) {
return 'motion-vendor';
}
if (id.includes('@radix-ui')) {
return 'ui-vendor';
}
};
它看起来很合理:React 放在一起,Three.js 放在一起,UI 库放在一起。
问题在于,这条规则只关心模块的名字,不关心模块属于哪个页面入口。项目中的 Sponsor 页面使用了 @react-three/fiber、@react-three/drei、Rapier 等大型依赖;About 页只需要 Three.js 核心和 GLTF/DRACO Loader。由于它们的路径都命中了 three 或 @react-three,最终被打进同一个 three-vendor。
About 页只要引用其中一部分,就必须把这个约 3.00 MB 的聚合包一起下载。

Astro 7、Vite 8 和 Rolldown 改变了什么
升级后,项目从 Astro 6.1.1、Vite 7.3.3 迁移到了 Astro 7.2.0、Vite 8.2.1,并使用 Rolldown 的分包配置:
build: {
rolldownOptions: {
output: {
codeSplitting: {
groups: [
{
name: 'vendor',
test: /node_modules[\\\\/]/,
entriesAware: true,
maxSize: 450 * 1024,
minSize: 50 * 1024
}
];
}
}
}
}
真正重要的是 entriesAware: true。新的分包不再简单地把所有带 three 的模块塞进同一个全局大包,而是考虑它们与不同入口之间的关系。
Sponsor 独有的 React Three/Rapier 代码不再污染 About 页,About 只加载自己所需的 Three.js 核心和 Loader。启动 JavaScript 从 3.26 MB 降到 744 KB,useEffect 和模型请求随之提前。这也是为什么单看 statue.glb 会找不到答案:模型一字节都没有减少,变化发生在模型请求之前。
构建时间也提供了一个旁证
升级前,Vercel 日志中的完整构建约为 8 分钟,其中 Astro Server Build 约 6 分 55 秒;升级后完整构建约为 4 分钟,Server Build 约 3 分 15 秒。
构建速度不能直接证明用户页面一定更快,但它说明模块分析和产物生成链路发生了实质变化。结合最终 chunk 体积,可以确认这不是一次只有版本号变化的升级。
这次排查给我的几个提醒
1. “公共 vendor 包”并不总是公共
按库名拆包很直观,但容易把“使用同一个生态”误认为“需要同一组代码”。两个页面都使用 Three.js,不代表它们应该共享整个 Three.js 生态包。
2. 体感上的 Loading,要沿着请求发生顺序分析
用户看到 Loading,不一定是模型文件太大,也不一定是接口慢。需要确认:真正的数据请求是在 HTML 到达后立即发出,还是要等一段 JavaScript 下载、解析、执行之后才会发生。
3. TTFB 很快,不代表页面已经完成
这次两版缓存命中后的 TTFB 都很快。如果只观察服务端指标,很容易得出“性能没有变化”的结论。真正的差异藏在客户端依赖图和 hydration 之前。
4. 性能优化应先保留可比较的部署
Vercel 的不可变 Deployment 很适合做这种回溯。不要只对比本地开发环境,也不要只测当前域名;保留升级前后的产物,才能检查响应头、入口脚本、依赖关系和真实 chunk 体积。
一个可复用的排查顺序
以后再遇到“页面突然变快或变慢”,我会按下面的顺序检查:
最后
这次升级最有价值的地方,并不是让我得到一句“Astro 7 更快”,而是重新验证了一个前端性能里很容易被忽视的问题:
页面加载速度,不只取决于资源有多大,也取决于浏览器必须先等待哪些资源。
旧版本里,About 页在请求真正需要的 3D 模型之前,先为另一个页面的依赖付出了代价。新的按入口分包移除了这段错误的等待,于是用户看到的结果非常直接——Loading 还没来得及被注意,页面已经打开了。
这不是错觉,而是一条关键加载路径真的变短了。




