Next.js服务渲染性能
May 22, 2026
Next.js 核心优势是混合渲染(静态 / 动态),但如果混用不当、配置错误,会直接导致页面加载慢、服务端压力大、首屏卡顿等性能问题。
之前的个人项目使用了Next.js框架,仅考虑了功能的完成度,在性能方面没有完整考虑,所以导致性能不佳,最近有时间做了些性能上的优化,尤其在ISR、SSG渲染相关问题上遇到了不少问题,记录如下。
问题发现
如何判断页面是否真正开启了SSG/ISR。
- 编译阶段
next build 完成后会给出各个路由/页面的编译结果,其中也包含了渲染形态。
符号 含义
ƒ 动态 SSR(每次请求实时渲染)
● ISR(静态生成 + 定时重验证)
○ 纯静态 SSG(构建时生成,永不重验证)
λ 旧版 Next.js 的动态标志
Next.js的原则是能静态(ISR静态增量、SSG纯静态)尽量使用静态,否则访问量大时可能达到性能上的瓶颈。
- 现网观察http返回header
编译结果不代表真正的访问结果。部署后发现访问页面时的Cache-Control值不对,返回为 private, no-cache, no-store, max-age=0, must-revalidate ,这跟文档直接描述不一致:
Incremental Static Regeneration (ISR) sets the Cache-Control header of s-maxage: <revalidate in getStaticProps>, stale-while-revalidate. This revalidation time is defined in your getStaticProps function in seconds. If you set revalidate: false, it will default to a one-year cache duration.
ISR实际未生效,还是动态渲染。ISR的返回应该是:
Cache-Control: s-maxage=60, stale-while-revalidate
优化
导致ISR/SSG未生效的原因可能包括多种。包括:
- 使用了动态API:比如header、cookies、searchParams、noStore等
- fetch配置问题
- export const dynamic = 'force-dynamic'显式强制 SSR
- 其他
这里遇到的几个典型问题详细分析。
动态API
使用了 next/header
即使只是读取 header/cookies 也会导致页面变为动态渲染,Next.js 无法在编译时知道你是否用 headers 改变了页面内容。所以只有出现相关api调用就会降级为动态渲染。
项目内容页面为了服务端跳转方便,在某个服务端组件中使用了从 header 取域名的方式,只要使用到了 header ,next server渲染方式就会变成dynamic。
使用以下 API 会自动将路由标记为动态:
- cookies()
- headers()
只要用一个 → 整页变成动态渲染。父布局用了 → 所有子页面全部动态。
缺失 generateStaticParams()
很显然,没有参数无法在build时生成。但是看文档好像也不是必须,不知道是否跟Next.js版本有关。
对于SSG页面,如果没有此方法调用,而是指定了revalidate,那么页面会变为ISR。如果二者都没有指定则又会默认为SSG。
使用了searchParams
文章翻页时将翻页ID写入了 searchParams: `page=3&limit=10`
在 App Router 里,只要 Server Component 直接用了 searchParams,页面会被强制变成「动态渲染(SSR)」,SSG/ISR 直接失效。注意Pages Router 不受这个影响,但也不会把 query 做进静态缓存 key。
- Server Component 解构 searchParams → 页面自动 dynamic = 'force-dynamic'
- dynamic = 'force-dynamic' → 不能 SSG、不能 ISR,只能每次请求都 SSR
// app/page.tsx
export default function Page({
searchParams, // ❌ 服务端组件直接拿
}: {
searchParams: Record<string, string>
}) {
return <div>{searchParams.q}</div>
}
为什么 Next.js 这么设计:
- searchParams 是请求级、用户级、任意多组合
- 静态缓存(SSG/ISR)是「一个路径 → 一个 HTML」
- query 无穷多,没法在 build 时预生成,也没法稳定缓存
解决方法:
想保留 ISR,又要读 query:正确做法应该是把读 searchParams 放到 Client Component,服务端只做静态渲染:
// app/page.tsx(服务端,可 ISR)
export const revalidate = 60; // ✅ ISR 生效
import SearchContent from './SearchContent';
export default function Page() {
return <SearchContent />;
}
// app/SearchContent.tsx(客户端)
'use client';
import { useSearchParams } from 'next/navigation';
export default function SearchContent() {
const searchParams = useSearchParams();
const q = searchParams.get('q');
return <div>Search: {q}</div>;
}
项目中我选择了绕过 SearchParams 的方法,把page参数放到URL path中。比如 xxx.com/page/1。
编译结果一定准确吗
部署过程中,发现编译结果显示为 ISR,但是实际访问时的 Cache-Control 值明显不对。此时可以采用debug编译的方式: next build -d
debug编译时会给出详细的未生效信息,猜测是因为debug编译进行了更严格的检查校验。
>> next build -d
Static generation failed due to dynamic usage on /zh-CN/notes/0b03eea7-bb82-4120-82e1-42a357742d1c, reason: headers
Static generation failed due to dynamic usage on /zh-CN/notes, reason: searchParams.page
排查后发现实际在页面的某个组件里面间接使用到了 cookie。
Page(文章页)
└─ Layout(lobby/layout.tsx)
└─ SiteHeader
└─ getCacheUser()
└─ createClient()
└─ cookies() ← 💥 这里触发动态渲染
lib/supabase/server.ts 中调用了 cookies(),这是 Next.js 的动态函数(Dynamic Function)。任何组件树中只要有一个地方调用了 cookies() / headers() / searchParams,整个路由就会被强制降级为动态渲染,无论页面本身配置了什么 revalidate 或 force-static。
解决方法:
- 强制改为静态渲染试试
export const dynamic = 'force-static';
参考文档:
Forces a static rendering and caches the data of a layout or page by returning an empty values for cookies(), headers() and useSearchParams(). Causes revalidate to be set to false.
即页面生成之后永远不更新。即使设置revalidate也无效。
修改后发现确实生效了,但是引入了新的问题:无法获取用户信息。
force-static 会强制将所有动态函数(cookies()、headers())返回空值,即使在 Suspense 隔离的子组件里也一样。所以 getCacheUser() 拿到的 cookie 是空的,supabase.auth.getUser() 自然返回 null。
所以如果某些组件依赖动态函数,可能获取到的数据会跟想要的不一致了。
中间件会有影响吗
由于使用了supabase,在middleware里面会触发票据更新操作,这会涉及cookie的读写。是否会影响渲染逻辑?
middleware 里的 cookies() 操作不会影响页面的静态/动态判断(middleware 运行在独立的 edge runtime,不在渲染树内)。
数据fetch的问题
项目依赖从notion获取数据接口,所以很早就引入了redis作为数据缓存。Next.js中使用了自定义cache handler来降低开发复杂度。
找不到js文件
可能的原因:
- 构建产物与运行版本不一致(最常见)
浏览器或 CDN 缓存了旧 HTML,请求了已被新构建替换掉的 chunk。
rm -rf .next
npm run build
npm run start
- 服务端缓存问题(这里使用了redis)
检查 cacheHandler 如果 Redis 中缓存了旧的 HTML 页面内容,需要清空 Redis 缓存后重新访问
每次编译不会重新生成缓存key吗?理论上会,但实际上存在例外情况。
总结
- 想要 ISR(定期重新生成):去掉 force-static,只保留 revalidate
- 想要完全静态(构建时生成,永不更新):只保留 force-static,去掉 revalidate
- 想要静态生成 + 定期更新:只保留 revalidate,Next.js 有 generateStaticParams 的情况下会自动 ISR。
- 需要静态渲染,那么就将依赖cookie、headers的组件放入client组件。