大飞的技术博客

·2 分钟

Next.js 静态导出的边界与取舍

我的几个站全部用 Next.js 的 output: 'export' 静态导出:构建产物是纯 HTML/CSS/JS,扔到任意静态服务器就能跑,没有 Node 进程、没有服务器运维。 这个选择不是免费的,这篇文章把边界讲清楚。

换来了什么

  • 部署极简:out/ 目录整个拷上服务器/OSS 就完事,不需要 PM2、 不需要容器、不需要考虑 Node 版本漂移;
  • 天然抗流量:静态文件随便被 CDN 缓存,官网场景下几乎不存在 「被打挂」的概念;
  • 安全面小:没有服务端运行时,攻击面收缩到静态文件本身;
  • 构建期报错:所有数据获取都发生在 next build,内容错了构建直接 失败,坏内容到不了线上。

失去了什么

静态导出本质是把所有「动态」挪到构建期或客户端,以下能力直接不可用:

能力静态导出下的状态替代方案
服务端 API Routes不可用(除非 force-static)外部服务/表单代收
ISR 增量再生不可用重新构建重新发布
请求级动态渲染不可用客户端 fetch
中间件 Middleware不可用CDN 边缘规则
图片优化服务关闭(unoptimized)构建期压好图

对品牌官网和技术博客,这个清单几乎无痛:内容发布频率低(重新构建即 「ISR」),交互都在客户端,表单可以走第三方代收再通知到企微/邮件。

常用的「伪装动态」手法

1. Route Handler + force-static 生成派生文件。 RSS、sitemap 这类「构建期可确定」的动态产物:

// app/rss.xml/route.ts
export const dynamic = "force-static";

export async function GET() {
  return new Response(buildRss(), {
    headers: { "Content-Type": "application/rss+xml; charset=utf-8" },
  });
}

构建时执行一次,输出落成真正的 out/rss.xml 静态文件。

2. generateStaticParams 展开所有路径。 /posts/[slug] 这类动态路由必须在构建期穷举:

export function generateStaticParams() {
  return getAllPosts().map((post) => ({ slug: post.slug }));
}

忘了写这个函数,动态路由在静态导出下会被静默跳过——不报错,只是 产物里没有那些页面,发布后才发现 404。

3. 数据获取全部收口到构建期模块。 文件读进内存、校验、缓存,页面组件只消费纯函数结果。好处是测试 不需要起 Next,数据层单测就是普通 Node 测试。

判断 checklist

什么项目适合静态导出:

  • 内容发布频率低,发新内容 = 触发一次重新构建可以接受;
  • 没有按请求变化的个性化内容(登录态、A/B、地理定向);
  • 搜索/评论/表单这类动态能力愿意交给第三方或客户端方案;
  • 有 CI 能自动构建发布(没有的话,发布 = 手动跑命令)。

反过来,只要有任何一条硬性不满足——比如需要 SSR 做 SEO 的个性化 首页——就老实用 next start 或托管到 Vercel,别跟 output: 'export' 较劲,它的边界是设计如此,不是 bug。

一句话结论

静态导出适合「内容即产品」的站:官网、博客、文档站。它把复杂度从 运行时挪到了构建期,而构建期复杂度是靠 CI 和纪律管理的,比运行时 复杂度便宜得多。