Skip to content

Astro vs Next.js #138

@lmk123

Description

@lmk123

此博文已迁移至 https://www.micais.me/posts/2025/06/issue-138


先说结论

  • 如果要开发的网站大部分是静态内容(如个人主页、文档、博客等),用 Astro。
  • 如果要开发的网站是一个功能性的网站(aka Web App,比如用户中心、Todo Lists、AI Chat 等),用 Next.js。
  • 如果静态内容和功能性各占一半,那么推荐用两个网站来实现。
    • 常见做法是:www.example.com 用 Astro 开发,用作静态内容展示,然后 app.example.com 用 Next.js 开发,用作功能性网站。举例: www.netlify.com / app.netlify.com。
    • 如果一定要整合在一起实现,我推荐用 Next.js。

正文开始

现在如果要开始开发一个新的网站,那么我一般都是无脑 Next.js。但是,最近开发的一个网站让我的想法有所改变。

这个网站是一个软件的官网,分为以下几个部分:

  1. 首页 /、关于我们 /about 等普通页面
  2. 文档页面 /docs/xxx。文档用 Markdown 形式书写,并且需要能自定义界面和样式
  3. 博客页面 /blog/xxx。博客用 Markdown 形式书写,并且需要能自定义界面和样式
  4. 多语言支持。/blog 表示英语,/zh-CN/blog 表示中文,依此类推

Next.js 的问题

一开始,我也是使用了 Next.js,然后就遇到了一些问题。

由于网站大部分内容是 Markdown,我先是尝试使用了 Nextra,然后发现它并不适合我的需求:

  • 它的可定制程度不高
    • 虽然样式方面可以用 !important 覆盖,但 html 结构无法修改
  • 它更适合于用来做“整个网站就是个单独的文档站”的情况,而我的网站实际上是需要将“普通页面”、“文档页面”、“博客页面”这三个部分整合在一起,但又都要能自定义界面
    • Nextra 的默认样式是全局的,这意味着即使是首页这种跟文档无关的页面,也会需要用到跟文档页面统一的 header 和 footer
    • 博客的部分也用了 markdown,但我不希望是文档站的那种 sidebar 样式,而这个样式又无法自定义

遂放弃 Nextra,打算自己根据 How to use markdown and MDX in Next.js 来实现一个,然后又遇到了问题。

Next.js 虽然支持将 Markdown 渲染成 page,但它仅有渲染能力,而如果要做成一个完整的文档系统,我还需要自己补齐以下功能:

  • 自行读取一个文件夹下的所有 Markdown
  • 根据文件夹 markdown 结构自动生成 sidebar 导航栏
  • 自己编写动态参数 (例如 [...mdxPath]/page.tsx),然后根据参数读取 markdown 内容进行渲染
  • 自行完成对 meta 数据的处理,比如 发布日期作者头图链接 等。
  • 兼容其它部分(“普通页面”和“博客页面”)的多语言支持

以上这些部分,如果我自己做完,那差不多就约等于一个 Nextra 了。

开始尝试 Astro

这时候我想到了 Astro,之前曾看到过它,它的官网声称它的使用量比 Next.js 还大。

一开始我也使用了基于 Astro 的文档系统 Starlight,但是它跟 Nextra 有同样的问题。虽然 Starlight 的文档上有“覆盖组件”的能力,但实际使用起来其实能改的不多,大致的页面框架仍是固定了的。

然后我就开始翻阅 Astro 的文档来自己实现文档和博客系统,找到了 Content collections - Astro。刚开始看的时候觉得有点复杂,实际使用起来直呼“真香”。

前面提到,如果我要在 Next.js 中实现文档系统,我需要做很多事情,而 Astro 基本上已经都做好了。

  • 无需自行做任何配置,markdown 本身就是 Astro 支持的文件类型,只需要“导入”然后“作为组件使用”。
  • Content collections 帮助我完成了所有 markdown 的收集以及渲染工作。
  • markdown 本身就支持 meta 信息,Astro 称为 frontmatter YAML

我需要做的就只有一件事:把 url 里的动态参数映射到对应的文件路径,非常直观:

---
// /src/pages/[...locale]/blog/[id].astro

import { getCollection, render } from 'astro:content'

const defaultLocale = 'en'

// 1. 为每个语种 + markdown 文件生成一个对应的网址
export async function getStaticPaths() {
  const posts = await getCollection('blog')
  return posts.map((post) => {
    // id 是文件系统里的路径,比如 `zh-cn/blog-1`、`en/blog-1`
    const id = post.id
    const firstSlashIndex = id.indexOf('/')

    let blogLocale: string | undefined = id.slice(0, firstSlashIndex)
    // 对于默认语言,不要在 url 里显示语种,也就是:
    //  网址 `/blog/blog-1` 对应文件系统的 `/src/content/blog/en/blog-1.md`
    // 网址 `/zh-cn/blog/blog-1` 对应文件系统的 `/src/content/blog/zh-cn/blog-1.md`
    if (blogLocale === defaultLocale) {
      blogLocale = undefined
    }
    // 剥离掉语种部分,即将原本的 `zh-cn/blog-1` 和 `en/blog-1` 变成 `blog-1`
    const blogId = id.slice(firstSlashIndex + 1)

    return {
      // 最终,会为每个语种 + md 文件生成一个对应的网址,例如:
      // `/zh-cn/blog/blog-1`
      // `/blog/blog-1`
      params: { locale: blogLocale, id: blogId },
      props: { post },
    }
  })
}

// 2. 使用渲染好的 markdown content 组件
const { post } = Astro.props
const { Content } = await render(post)
---

<div>Markdown 内容:</div>
<Content />

Astro 真的做到了“简化 md 收集和渲染”以及“完全自定义界面”。

示例里的中文语种用的是全小写的 zh-cn 而不是标准的 zh-CN,这是有原因的

在用 Astro 做多语言的时候,要用全小写的语种,比如 zh-cn,而不是 zh-CN,不然会报 404。

这是因为,Astro 在很多地方都做了小写转化,比如 astro:i18n 里的方法里(可以用 normalizeLocale: false 关掉),以及前面用到的 getCollection() 得到的 post.id,即使你的文件夹用的是 zh-CN/blog-1.mdpost.id 里也会是全小写的 zh-cn/blog-1,并且无法通过设置改变这一行为。

所以,最简单的办法就是语种也用小写,不然就要在很多地方都处理大小写问题。不过这样一来,URL 会是 /zh-cn/xxx 而不是 /zh-CN/xxx,现在大部分网站都是后者,但是可以观察到,使用了 Astro 网站都是小写的 /zh-cn/xxx,比如 https://www.cloudflare.com/zh-cn/

Astro 的问题

到目前为止,Astro 很好用,但是接下来,我想要做一些功能性的界面,例如登录 / 注册、用户中心等一些交互很重的界面,然后我就发现了 Astro 的不足。

Astro 本质上是一个“静态网页生成器”,它生成的是一个“多页面应用(MPA)”,这跟“单页面应用(SPA)”有很大不同,比如:

  • 全局状态管理:如果是 SPA,由于没有页面刷新,所以很容易就可以做到跨 URL 的统一状态管理;但是 MPA 就不同了,每次跳转都是一次刷新,你需要把状态存进 localStorage 里,或者每次都从服务器重新读取。
  • 交互性组件:Astro 可以使用交互性组件(或者在 Next.js 里叫 Client Component),但是实际上,你是在每一个页面都有一个(或多个) React App,而不是整个网站就是一个 React App。

这一点上,Next.js 就做了一个很好的折衷:首次加载会带有完整的静态 HTML,用户能快速看到内容,但是之后就会变成一个 SPA,有更好的使用体验;同时,它的开发过程会更接近在开发 SPA 的感受,你无需反复提醒自己“Astro 是个多页面应用,每个页面都是一个单独的 React App”。

架构上的不同之处

我们通过几个场景来解释 Astro 与 Next.js 在架构上的不同之处。

第一个场景:纯静态内容网站

假设我们的网站是一个纯静态网站,无任何 js,那么:

  • Astro 输出的内容将没有任何 js 代码,零 js;全程都是静态 html,页面间的跳转会触发浏览器刷新
  • Next.js 仍然会附带少量全局运行时 js 代码;首次加载是静态 html,但之后的页面跳转会变成 SPA 形式的客户端路由渲染,需要依赖 js

在这个场景下,Astro 无疑是完胜:

  • 完全不依赖 js,所以性能肯定比 Next.js 高
  • 开发体验上会比 Next.js 更好

第二个场景:30% 都是静态内容,70% 需要 js 进行交互

假设我们的网站只有首页、关于我们等页面是静态内容,其余都是交互性很重的页面,那么

  • Astro 会输出很多 js;页面间的跳转仍然是静态 html 跳转,但在这种情况下,这反而会造成状态共享、逻辑混乱
  • Next.js 的优势就体现出来了,因为本质上是一个重 js 的网站,所以少量的全局 js 运行时相比起来可以忽略不计,客户端路由也成了优点

Astro 的“群岛架构”与 Next.js 的“混合架构”

两者有相似之处(都利于 SEO、都能避免用户先看到空白再看到内容),但本质上不同:

  • Astro 的“群岛架构“是,输出的全是静态 html 页面,而 html 页面里的交互部分是一个个小型的 SPA
  • Next.js 的“混合架构”是,它本质上还是一个大的 SPA,只是首屏加载时用的是提前拼好的 HTML 片段,避免了用户先看到空白、再看到内容的问题

总结

一句话描述:Astro 在“内容网站” + “少量功能性交互”方面做到了极致,而 Next.js 在“大量功能性交互” + “少量内容”网站方面做到了很好的折衷效果。

于是就有了开头的结论。

希望未来会有一个 Next.js 插件,把 Astro 的 Content collections 功能移植到 Next.js 里。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions