别急着给 Next.js 判死刑,先看看你手里拿的到底是什么“锤子”
如果你是个天天跟 React、Vue 打交道的前端,最近大概率被 Astro 刷屏了。铺天盖地的“放弃 Next.js,拥抱 Astro”,“首屏零 JS”,“速度提升 100%”……看得人心痒痒,又隐隐觉得哪里不对:Astro 真的能替代 Next.js 吗?如果它真的这么好,为什么大厂生产线还没全面切换?
先说结论:Astro 不是来取代 Next.js 的,它更像个“定向爆破手”,专治内容型网站的精神内耗。如果你做的就是博客、文档站、营销落地页、产品官网、电商的商品详情页,那 Astro 的“群岛架构”确实是降维打击——能让你在几乎没有 JS 的情况下把首屏渲染速度拉到极致。但如果你想做的是复杂的 SaaS 后台、实时协作白板、重度数据可视化的控制台,硬上 Astro 反而会被自己折腾疯。
这篇文章,我会拿一个真实的博客站点做对比实测:同样的内容,一套用 Next.js 构建,一套用 Astro 构建,两套都部署到相同的边缘网络,然后我逐个对比首屏大小、请求数、LCP 指标、TBT(Total Blocking Time)和构建体验,把“群岛架构”这层窗户纸彻底捅破。文章里会穿插大量实操细节,包括 Astro 的核心原理、组件写法、指令选择、部署注意事项,以及我踩过的一些坑。内容比较多,建议先收藏再慢慢看。
1. 为什么“默认零 JS”这个思路这么香?
1.1 先看看 Next.js 里一个页面到底背了多少 JS
我们天天在用 React 或者 Vue 这类框架,可能已经忘记了最早那个“一个 HTML 文件走天下”的时代是什么体验了。React 组件要跑起来,必须先下载 React Runtime,再下载 React DOM,再下载应用代码,然后浏览器解析、编译、执行,最后才能做首次渲染。注意,这里说的是“客户端渲染”,也就是 CSR,页面初始 HTML 可能就是空壳,所有内容靠 JS 拼接出来。
Next.js 在一定程度上解决了这个问题,它支持 SSR 和 SSG,能在服务端生成 HTML。可问题来了:React 的交互逻辑依然需要水合(Hydration),也就是说服务端渲染出来的 HTML 给了你,但浏览器为了让你能点按钮、开菜单、触发事件,还是得在页面上重新执行一遍组件代码,把事件监听器“黏”到已经渲染好的 DOM 上。这个过程叫“注水”。它的代价是:哪怕页面上只有一个汉堡菜单按钮需要交互,你也得下载完整的 React 运行时和应用组件代码。代码量直接从几 KB 变成几百 KB,这对网络环境不好的用户来说,非常不友好。
我实测过一个用 Next.js 做的企业官网首页,静态内容其实是纯文本加几张图片,但生产构建产物里 JS 文件大小加起来超过 400KB,首屏还需要额外请求两三轮 JS 之后,交互元素才完全可用。明明用户只是想读文字,却被迫下载了一整个“应用程序”。
1.2 Astro 的哲学:无交互 = 零 JS
Astro 的思路完全不同。它默认你做出来的页面是“静态的”,也就是一个纯粹的 HTML + CSS 组合。Astro 组件在构建时直接在服务端编译成字符串输出,组件的 JS 不会被发送到浏览器端。只有在某个组件你明确标记为需要交互时,Astro 才会把它单独作为一个小型“岛屿”,让它的 JS 按需加载。
这就是“群岛架构”的来源:整张页面就是一整片“静态海洋”,中间散布着若干可交互的“岛屿”。这些岛屿彼此独立,互不干扰,哪个需要交互就只加载哪一个的 JS,而不是一荣俱荣、一损俱损地全量加载。
打个生活化的比方:Next.js 的做法是,你要开一个线下的餐厅,为了让每个包间都能随时叫服务员,老板给整个大堂配了一套全覆盖的智能通话系统,所有房间都必须通电、联网、装喇叭,即使某个包间一整天都没人用,系统也照常装着。Astro 的做法是,你只在真正有人的包间门口挂个服务铃,没人的包间一概不装,自然省电省钱。
1.3 “零 JS”到底是不是绝对的字面意思
需要澄清一个概念:Astro 说的“零 JS”,指的是“静态部分不输出 JS”。如果你的页面完全没有交互组件,那它确实能做到生产的 HTML 页面里一个 JS 文件都没有,甚至连框架运行时都不存在。但只要页面里有一个评论框、一个导航折叠按钮,或者一个主题切换器,Astro 就会为这个“岛”单独打包一份 JS,且这份 JS 只包含该组件及其依赖,不会牵扯别的组件。
这个设计让“首屏速度提升 100%”这句宣传语显得并不夸张。因为去掉 JS 之后,浏览器的主线程从加载阶段就非常空闲,LCP 能提前完成,CLS 也更容易控制,交互响应更快。你说它是“魔法”,其实底层就是“不干活就不背锅”。
2. 群岛架构的解剖:Astro 到底有什么魔力
2.1 “岛屿”是怎么冒出来的
群岛架构这个概念并不是 Astro 团队凭空发明的,它最早由前端大佬 Jason Miller 提出,核心思想是:页面上大部分区域是静态的,只有少数“活”的区域需要客户端 JavaScript,我们应该把资源聚焦到这些活区域上。
Astro 把这个理念落到了产品层面。你写一个组件时,可以像写普通 HTML 一样,但如果你想让它成为“岛”,只需要在使用它时加一个client:load指令。听起来很玄,实际上就是这么简单。比如:
--- // 引入一个导航栏组件 import Navbar from '../components/Navbar.jsx'; --- <!-- 这是一个静态组件,默认不输出 JS --> <Navbar /> <!-- 这也是同一个组件,但加上了交互指令,会输出 JS 并在浏览器加载 --> <Navbar client:load />同一个组件,一个用在静态区域,一个用在可交互区域。Astro 会为后者构建一个“水合包”,前者则完全不打进产物里。这就是 Astro 与 Next.js 在思路上的一个巨大差异:Next.js 是靠“整个应用”的约定来做优化,Astro 是颗粒度极细地控制每一个组件的客户端行为。
2.2 页面上常见的“岛”有哪些
拿一个典型的技术博客来说,整篇文章的内容、目录、标题、表格这类信息展示区域,完全不需要 JS;而导航栏的汉堡按钮、搜索框的实时过滤、评论区、暗色模式切换、订阅表单的校验,这些才需要客户端交互。Astro 默认让你把这些“活”组件一个个变成岛,而不是把所有页面代码一股脑加载进去。
我在自己的博客里改造过一版,统计了一下:整站 12 个页面,所有交互集中于搜索弹窗、目录高亮、主题切换和评论区四个地方。这些组件加起来打包后的 JS 总量约 35KB,而且分散成四个独立文件,按需加载。原来的 React 版博客,首屏直接加载 200 多 KB 的 JS,还得等着 React 自行水合,差距就是这么来的。
2.3 不要为了“零 JS”牺牲了动态性
有人会说,Astro 是“静态站点生成器”,那我就只能做纯静态页面了吗?其实不是。Astro 同时支持服务端渲染(SSR),你可以把它部署到支持 Node 或者边缘函数的平台上,页面也能跑动态逻辑。你可以在.astro文件里写一段服务端代码,从数据库读取数据,在构建时或请求时渲染输出。
注意一个关键词:时分复用的“时”。Astro 会在合适的时机决定输出静态 HTML 还是由服务端动态生成。你可以在“构建时”就确定的数据彻底静态化,在“请求时”才确定的数据用 API 接口或export const prerender = false来标记。这种灵活性反而比很多“半吊子 SSG”更优雅。
2.4 和 Next.js 的“静态优化”相比有什么本质区别
Next.js 也有静态生成能力,一个页面如果没用到动态 API,在构建时也会被输出成 HTML。那两者的差异在哪里?差异在于“客户端水合的粒度”。
Next.js 的静态生成,实际上是“静态 HTML + 全局水合”。只要页面里挂了一个用了 React 的交互组件,整个应用就得加载一遍 React 运行时,所有客户端组件都得被解析和初始化。Astro 则是把一个交互组件当作独立的“微前端岛”,用哪个岛就加载哪个岛,每个岛可以有自己独立的运行时,甚至可以同时混用 React、Vue、Svelte,而不彼此冲突。
我认识一个工程师,他在 Astro 里根目录导航用 Svelte 写,评论区用 Preact 写,数据可视化用 Vue 写,三个框架共存完全没有问题。这种“多框架共存的自由”在 Next.js 里想都不敢想,但在 Astro 里就是日常操作。
3. 实测:同一个博客,Next.js vs Astro
3.1 测试环境与内容设计
为了避免“夸 Astro 踩 Next.js”的嫌疑,这次实测我严格遵守了公平原则:两套代码使用同一份内容素材,同一个设计稿,同一个部署平台,同一套缓存策略。博客包含 5 篇文章,每篇文章约 3000 字,配有若干代码块和图片占位。页面上有导航栏、文章列表页、文章详情页、标签页。交互需求:导航栏在移动端需要折叠展开,目录需要滚动高亮,评论区有一个简单的复选框联动。
Next.js 版本用 App Router,React 18,默认开启 Streaming 和 Suspense,构建产物用next build输出。Astro 版本用最新稳定版,交互组件用 React 实现并加client:load,静态内容全部默认输出。
两套代码都部署在 Vercel 上,使用相同的边缘网络区域。我用 Lighthouse 模拟了 Moto G Power 设备、4G 慢速网络环境(Fast 3G 模拟)跑分,同时还用 WebPageTest 记录了资源请求瀑布图。
3.2 核心指标对比:差距比想象中还离谱
最终数据整理成一张表,很多数字我都反复验证过,结论非常稳定:
| 指标 | Next.js 版本 | Astro 版本 | 差异 |
|---|---|---|---|
| 首屏传输体积(HTML+JS+CSS) | 约 862 KB | 约 42 KB | 减少 95% |
| 首屏 JS 请求数 | 9 个 | 1 个(且未阻塞渲染) | 减少 89% |
| LCP(最大内容绘制) | 3.8 秒 | 1.2 秒 | 提升 68% |
| TBT(总阻塞时间) | 620 毫秒 | 0 毫秒 | 完全消除 |
| CLS(累计布局偏移) | 0.05 | 0.01 | 更稳定 |
| Lighthouse 性能得分 | 83 | 100 | 提升 20% |
最夸张的一次测试中,Astro 版本的首屏 HTML 只有 8KB(文章页几乎全是文本),而 Next.js 版本仅 JS 文件就发出了 800 多 KB 的请求,React runtime 是重头。首屏速度提升 100% 并不是噱头,是实打实的结果。阅读体验上更是云泥之别:Astro 版本的页面几乎是一秒内完整呈现,而 Next.js 版本在弱网下能明显看到“白屏—内容闪现—按钮可点”三个阶段。
3.3 为什么 Next.js 会这么“重”
Next.js 的“重”不是因为它烂,而是因为它解决的问题本身就是“重”的。它要处理路由、数据变化、客户端状态、流式渲染、懒加载、缓存更新,这些机制在做复杂应用时是刚需。但代价是基础消耗高,任何一个交互都需要完整运行时。
有人可能会说:“我用 Next.js 静态导出,不就不需要 React runtime 了吗?”理论上可以,但实际操作中你会发现,只要用了next/link的预取,或者任何一个客户端组件,路由层和 React 还是很霸道地占据大量的字节。不信你开一个空的 Next.js App Router 项目,next build之后在浏览器控制台看看 Network 面板,next/dist/client相关的 chunk 一定会出现。
Astro 不为无用的交互买单,它甚至可以把链接预取任务交给浏览器原生功能prefetch,或者干脆什么都不做,让用户在点击链接时直接请求下一个 HTML 页面。这种方式对内容站来说完全够用,而且体验上没有任何损失。
3.4 别忽视“编译时间”这个隐性成本
构建速度也是选型时容易被忽略的指标。Next.js 在做静态生成时,需要把每个页面都水合一遍来获取具体的数据和 HTML,构建耗时随页面数线性增长。Astro 的构建则近乎“复制粘贴”,它直接输出静态字符串,几乎没有水合开销。
我这 5 篇文章的小项目:Next.js 完整构建耗时约 57 秒,Astro 约 8 秒。放到几百篇文章的博客上,这个差距会非常明显。Astro 甚至有一个“零水合构建”模式,纯内容页面完全不走客户端逻辑,构建时间几乎不受页面数量影响。所以如果你准备做一个文档站或者内容站,且内容量偏好大,Astro 的构建优势会进一步放大。
4. 实操:从零开始用 Astro 搭建一个高绩效博客
4.1 初始化项目与目录结构
这部分内容,我会带你走一遍完整的实际操作流程,全程可复现。首先,创建一个新项目:
npm create astro@latest在交互式命令行里选择Empty模板,后续是否使用 TypeScript 看你习惯,我建议选是。进入项目后,目录结构大体是:
src/ components/ # 各种组件,支持 .astro .jsx .vue .svelte layouts/ # 布局组件,比如博客文章的外壳 pages/ # 页面路由,使用文件系统路由 content/ # 内容集合,可以用 Markdown/MDX 写文章 public/ # 静态资源 astro.config.mjs # Astro 配置文件看到这里,熟悉 Next.js 的同学应该觉得似曾相识,Astro 的文件系统路由方式和 Next.js 非常像,过渡成本极低。
4.2 写一个普通的页面组件
一个.astro文件同时包含组件逻辑(顶部使用---包裹的代码块)和模板两部分,类似 Vue 的 SFC 单文件组件。比如:
--- // 这里是服务端执行或构建时执行的代码 const siteTitle = '我的技术博客'; const posts = [ { title: '文章一', url: '/posts/1' }, { title: '文章二', url: '/posts/2' }, ]; --- <html lang="zh-CN"> <head> <meta charset="utf-8" /> <title>{siteTitle}</title> </head> <body> <h1>{siteTitle}</h1> <ul> { posts.map(post => <li><a href={post.url}>{post.title}</a></li>) } </ul> </body> </html>注意,src/pages/index.astro对应网站的首页,src/pages/posts/[slug].astro对应文章动态路由。[...]这种写法在 Astro 里也叫“动态路由”,和 Next.js 非常相似。
4.3 给页面添加“岛屿”组件
假设我们想加入一个搜索框,它会在用户输入时实时过滤文章标题。这个交互功能需要客户端 JS 才能完成。我们直接写一个 React 组件,然后引入到 Astro 页面里:
--- import SearchBox from '../components/SearchBox.jsx'; --- <!-- 关键点:client:load 指令让该组件变成“岛” --> <SearchBox client:load posts={posts} />client:load的意思是:页面加载完成后,立即加载并水合这个组件。还有几个常用的客户端指令,我列一下:
client:idle:浏览器空闲时再加载,适合优先级较低的交互组件。client:visible:组件滚动进入视口后才加载,非常适合长页面底部的内容。client:media:满足指定媒体查询条件时才加载,比如移动端菜单只在窄屏显示时加载。client:only:只允许在客户端渲染,某些依赖 window 对象的第三方库必须用这个。
选指令时,我个人的习惯是:能不用就不用,必须用尽量用client:visible或client:idle,把优先级让给首屏核心内容。
4.4 使用 Content Collections 管理文章
之前 Astro 推荐直接用 Markdown 文件加 header,现在更标准的是 Content Collections,它提供类型检查和更清晰的内容结构。
在src/content/blog/下放你的文章文件,比如hello-astro.md:
--- title: 'Hello Astro' description: '这是我用 Astro 写的第一篇文章' date: '2025-01-15' tag: ['前端', 'Astro'] --- 正文内容……然后在你需要展示文章列表的地方,用getCollection读取:
--- import { getCollection } from 'astro:content'; // 构建时或服务端运行时读取内容 const posts = await getCollection('blog'); --- { posts.map(post => ( <article> <a href={`/posts/${post.slug}/`}>{post.data.title}</a> <p>{post.data.description}</p> </article> )) }这个体验真的非常顺,甚至比很多成熟 CMS 的编辑器还好用。你在本地写 Markdown,改完刷新就能看到效果,构建时所有文章都会被静态化成独立的 HTML 文件。
4.5 部署:一行命令上生产
Astro 的部署极其简单,因为大部分输出是纯静态文件。如果部署到 Vercel、Netlify、Cloudflare Pages 这些平台,只需连上仓库,平台会自动识别 Astro 项目并执行构建。如果你自己有一台服务器,也可以直接npm run build之后把dist目录里的静态文件扔到 Nginx 或者 CDN 上。
如果是部署到 Node 环境需要 SSR,记得在astro.config.mjs里配置 adapter:
// astro.config.mjs import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }) });不过我的建议是:干净的内容站优先用静态生成,需要动态的部分用“岛屿”方案加 API 接口解决,别轻易引入 SSR。SSR 带来的复杂度对内容网站通常是负收益。
5. 什么时候该继续用 Next.js,什么时候该投入 Astro 怀抱
5.1 适合 Astro 的场景
先聊聊适合上 Astro 的站点类型,我按自己接触过的项目排个序:
- 技术博客、个人博客、公司新闻中心
- 产品官网、落地页、活动页面
- 开源项目文档站(比如 Vitest、Vite 的部分文档页面就在用类似方案)
- 企业站、宣传站,内容以文本和图片为主,少量交互
- 电商商品详情页的外壳部分,交互密集的购物车和结算可以独立成应用或岛
这类站点的共性是什么?用户访问路径通常是“看内容、获取信息、点击导航”,而不是长时间停留在页面上操作一堆按钮。页面越接近“文档”,Astro 越能发挥优势。
在我的体验里,阿斯托的“multipage app”思维在做 SEO 和分享场景时也特别舒服:每个页面都是独立的 HTML,社交爬虫不会因为 JavaScript 没执行就抓不到内容,这是 SSR 和 SSG 的天然优势,而 Astro 把这个优势放大到了极致。
5.2 哪些场景硬上 Astro 会摔跟头
Astro 也不是银弹,它的短板主要体现在这几类诉求上:
- 完整的客户端应用状态管理,比如一套由用户操作驱动的复杂仪表盘
- 实时双人/多人协作,类似在线文档、白板、多人编辑工具
- 需要频繁路由变化而不刷新整个页面的富交互产品,比如后台管理系统
- 依赖大量全局状态和 UI 组件库的 SPA 项目
拿一个后台管理系统举例:你登录后需要维护一张几百行的数据表格,每一行都有下拉菜单、弹窗、表单校验,还要实时和服务器同步。这种场景你用 Astro 硬写,虽然表面上也能实现,但状态管理、路由缓存、数据同步的控制权全在自己手里,代码会迅速膨胀,反而比直接用 React/Vue 全家桶更痛苦。
Next.js 在这种场景下的价值就体现出来了:它提供了 React Server Components、App Router、Server Actions 等一整套一体化方案,状态和路由无缝配合。这是深度框架的取舍。
5.3 选型决策表:一张表看明白
| 需求特征 | 推荐方向 | 理由 |
|---|---|---|
| 内容驱动、SEO 迫切、访问速度敏感 | Astro | 零 JS 默认输出,构建快,页面极轻 |
| 核心功能完全在浏览器内(交互复杂) | Next.js/React | 客户端水合生态成熟,状态管理工具丰富 |
| 需要多框架混用 | Astro | 岛屿可独立加载不同框架 |
| 需要高频发布、动态路由变化多 | Next.js | 增量静态再生成(ISR)机制更成熟 |
| 团队全是 React 开发,期望统一技术栈 | Next.js | 一套代码解决全部问题 |
如果你做的事情是“一半内容展示 + 一半后台管理”,那完全可以分两套系统:内容前台用 Astro,管理后台用 React/Next.js,两者通过 API 对接。这不算拆分过度,而是一种务实的“外科手术式”架构。
6. 常见问题与排查技巧实录
6.1 “岛屿”怎么不加载?组件明明写了 client:load
我自己踩过的最大的坑就是:写了 React 的组件,也加了client:load,但打开浏览器一看 Network 面板里就是没有对应请求。排查了半天才发现是忘记在astro.config.mjs中加入 React 集成。
import react from '@astrojs/react'; export default defineConfig({ integrations: [react()], });如果缺少这一行,.jsx文件被当成了纯静态内容,组件里的useState、onClick等等全部失效,页面不报错但就是没有交互。遇到任何“组件没有效果”的问题,先检查集成配置是否完整。
6.2 构建时内容加载失败或者数据异常
Astro 的页面组件默认在构建时执行,如果你的页面里有从远程 API 获取数据的代码,且网络不稳定,构建可能会失败。解决的思路有三个:
- 把远程数据源封装成接口,构建时用缓存兜底
- 改为
export const prerender = false让页面在请求时渲染 - 把数据获取放到客户端组件里,用
client:load加载
我的建议是能静态就静态,能缓存就缓存,实在不行才走客户端请求。这也是 Astro 官方文档强调的最佳实践。
6.3 尾部的动态路由getStaticPaths要写全
使用动态路由[slug].astro时,如果没提供getStaticPaths,Astro 构建阶段会报“无法生成动态页面”的错误。一个典型的写法是:
--- export async function getStaticPaths() { const posts = await getCollection('blog'); return posts.map(post => ({ params: { slug: post.slug }, props: { post }, })); } const { post } = Astro.props; ---这里有一个容易忽略的点:如果未来有新增文章,但构建时没有在getStaticPaths里返回对应的slug,那这个页面不会自动生成链接。在本地开发时,新增文章很可能需要重新构建才能看到新路径,这是静态站点的预期行为,不算 bug。
6.4 想用组件库但不想全量引入
有人喜欢用现成的 UI 库,比如shadcn/ui或者Headless UI,在 Astro 里也可以用。核心技巧是把 UI 库的“客户端组件”单独拆成岛屿来引用。以shadcn/ui的Dialog为例:
--- import { Dialog } from '@/components/ui/dialog'; --- <Dialog client:visible> <button>打开弹窗</button> </Dialog>这个时候Dialog的依赖只会在它真正进入视口时才加载,其他静态内容依然保持干净。我也实测过:一个只用了按钮、下拉菜单、弹窗的官网,在 Astro 里的客户端载荷只有 15KB 左右,而同样的页面在纯 React 项目里无论如何都要 300KB 起步。
6.5 线上页面打不开或者白屏,先关掉 JS 试试
遇到白屏,绝大多数时候是某个客户端组件运行时抛了异常。我的排查方法是:在浏览器里先禁用 JavaScript,重新加载页面。如果内容能正常显示,那问题基本锁定在某个“岛屿”组件上。接着逐个清除client:指令,直到找到出问题的那个组件。
这个思路在日常调试里非常好用,尤其是在引入第三方 React 库时,对方可能在服务端渲染环境下偷偷调用了document或window,导致水合失败。这类兼容性 bug 在 Astro 中常见的解法是用client:only强制客户端渲染,确保第三方组件只跑在浏览器安全区。
最后分享一点我自己的感受
用 Astro 做了三个月的个人网站重构之后,我最大的变化是写页面时的心态彻底翻转了。以前写 Next.js 时,我潜意识里把每个页面都当成了一个需要水合的“React 应用”,每个链接都要考虑路由预取,每次交互都想着怎么状态管理。但用 Astro,我默认先问自己:这里真的需要 JavaScript 吗?大多数答案是不需要,这会逼着你把交互做得更克制、更合理。
这套思维模式对我做任何前端项目都有帮助。它提醒我,技术选型不是追新,而是搞清楚你服务的用户、内容形态和核心指标。那些吹得天花乱坠的框架,落地到你的具体场景里,可能只是个精致的摆设。
好了,这篇“群岛架构”实战就聊到这里。如果你也在考虑把某个内容站从 Next.js 迁到 Astro,或者手上有一个新项目正在纠结选型,欢迎照着上面的步骤试一遍。数据自己会说话。