欢迎光临
我们一直在努力

从 Loading 到秒开:一次 Astro 7 升级背后的分包性能复盘

hero

最近我把博客从 Astro 6 升级到了 Astro 7。部署完成后,最直观的感受不是某个新 API,也不是构建日志里的版本号,而是:页面像是突然“秒开”了。

特别是 About 页面。以前进入页面时,总能明显看到一段 Loading;升级后,这个等待几乎一闪而过。

一开始我也怀疑这只是缓存、网络状态或者刚部署后的心理作用。于是我保留了升级前后的两个不可变 Vercel Deployment,从服务端响应、静态资源和客户端依赖链三个层面做了一次对比。

最后发现,真正影响体感的并不是“Astro 7 天生更快”,而是升级过程中发生的一次分包策略重构:旧配置把本不属于 About 页的大量 Three.js 生态代码塞进了它的首屏依赖,新配置则按照入口重新隔离了这些模块。

先说结论

下面的体积来自 Vercel 上两个不可变生产部署的已最小化(minified)代码产物,但没有启用 HTTP gzip/Brotli,因此更接近浏览器解压后需要解析的体积。实际网络传输会更小,不过 JavaScript 下载后的解析和执行负担仍然存在。

Astro 7 升级前后的关键加载体积对比

其中 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 的聚合包一起下载。

旧 manualChunks 与 entriesAware 分包的依赖路径对比

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 体积。

一个可复用的排查顺序

以后再遇到“页面突然变快或变慢”,我会按下面的顺序检查:

  • 确认对比版本、区域和缓存状态一致;
  • 对比 TTFB、HTML 和关键 CSS,先判断是不是服务端问题;
  • 从 HTML 找出所有 client:load、client:only 和入口脚本;
  • 递归追踪静态 import,而不是只看入口文件大小;
  • 找出请求真正数据或模型之前必须完成的同步链;
  • 检查 manualChunks 是否造成跨入口污染;
  • 最后再用 Lighthouse、Chrome Performance Trace 或真实用户数据验证 LCP、INP 和长任务。
  • 最后

    这次升级最有价值的地方,并不是让我得到一句“Astro 7 更快”,而是重新验证了一个前端性能里很容易被忽视的问题:

    页面加载速度,不只取决于资源有多大,也取决于浏览器必须先等待哪些资源。

    旧版本里,About 页在请求真正需要的 3D 模型之前,先为另一个页面的依赖付出了代价。新的按入口分包移除了这段错误的等待,于是用户看到的结果非常直接——Loading 还没来得及被注意,页面已经打开了。

    这不是错觉,而是一条关键加载路径真的变短了。

    赞(0)
    未经允许不得转载:171主机测评 » 从 Loading 到秒开:一次 Astro 7 升级背后的分包性能复盘
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址