Skip to content

Astro vs Next.js #138

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 里。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions