ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

成为全栈·Next.js 网站前台篇·SSR、SSG、ISR 与动态渲染:不要给整站贴一个标签

2026/9/28 20:43:23 拓冰建站 浏览量
成为全栈·Next.js 网站前台篇·SSR、SSG、ISR 与动态渲染:不要给整站贴一个标签 成为全栈·Next.js 网站前台篇·SSR、SSG、ISR 与动态渲染不要给整站贴一个标签一个 Next.js 网站可以同时包含预渲染的公开内容、定时再验证的首页、按请求计算的搜索和完全私有的会员数据。渲染方式是页面的决策不是项目的标签。前言刚接触 Next.js 时我也喜欢问这个网站是 SSR 还是 SSG问题很像在选技术方案实际上却把一个网站简化过头了。首页文章变化不算频繁可以容忍短暂旧值搜索结果取决于 URL 关键词会员收藏属于当前用户sitemap.xml可以比首页更慢地更新。这些页面若强行用一种模式总有一类需求被牺牲。更准确的问法是这个路由的内容由什么决定可以旧多久是否包含用户私有数据先把四个词放回时间线方式主要生成时机内容更新适合什么SSG构建时重新构建部署极少变化的公开页ISR首次预渲染后按时间或事件再生成过期后再验证可短暂陈旧的公开内容SSR / 动态渲染请求到达时下次请求重新计算依赖请求或必须新鲜的内容CSRJavaScript 启动后由浏览器获取客户端重新请求高交互、私有或局部数据在 App Router 里这些模式不再完全等同于四种页面 API。路由使用哪些请求时 API、fetch选项和路由配置会共同决定何时渲染。当前项目也没有开启 Next.js 16 的cacheComponents标志因此使用的是fetch缓存选项与路由revalidate这套模型不能把新模型的use cache直接套进来解释。公开首页用可接受的旧值换稳定与速度首页明确设置了export const revalidate 60公开请求层也使用force-cache和 60 秒再验证constresponseawaitfetch(url,{cache:options.cache||force-cache,...(options.cacheno-store?{}:{next:{revalidate:60,...options.next}}),})管理后台发布一篇文章后公开站不必在同一毫秒就看到它。对这个内容站而言60 秒的可见性窗口是可接受的换来的是同一份公开内容不需每个访客都重新请求后端。这不是说首页每 60 秒主动刷新一次。过期只是让后续访问触发再验证。对低流量站点若过期后没有人访问也不会凭空运行一次生成。文章详情公开不代表永久静态文章详情页由 slug 定位内容很适合预渲染和缓存。但文章可能被修订相关文章、点赞数和上下篇也会变。把它做成一次构建后永不更新的 SSG意味着每次改文章都要重建整站。当前方案继续采用公开缓存和再验证同时把点赞、收藏、评论状态留给客户端。于是同一页里出现两种新鲜度文章正文公开缓存允许短暂旧值 当前用户是否点赞客户端查询按当前会话计算所以一个页面也不必只有一种数据策略。核心内容与个人互动的所有权不同渲染方式自然也不同。搜索页关键词本身就是请求数据搜索结果取决于searchParams关键词可能无限多结果又要反映最近发布的内容。当前的搜索 API 明确使用cache: no-store。这里不追求预生成所有关键词页也不把查询结果放进公共内容缓存。当搜索量变大时可以在搜索服务、CDN 或应用层引入专门缓存但那需要单独的命中率和失效策略不应该由首页的 60 秒规则顺手决定。会员中心私有数据必须从公共缓存中隔离会员资料、收藏、通知和投稿列表都属于当前会话。如果这些响应误用公共缓存问题就不只是“数据旧了”而是可能把 A 用户的结果变成 B 用户的响应。项目的同源代理因此对私有响应设置out.set(cache-control,private, no-store)会员页采用客户端会话恢复和私有请求。这不是因为 CSR 比 SSR 更安全客户端守卫也不是权限边界。真正的安全边界仍在后端鉴权private, no-store则防止中间缓存复用这份私有数据。sitemap 有自己的时钟app/sitemap.ts使用 300 秒再验证。它比首页的 60 秒更长因为搜索引擎入口不需要和站内列表同秒更新。这个差异很小却很能说明问题TTL 不是一个项目级常量它是数据新鲜度与生成代价之间的业务决策。构建输出是证据但不是唯一证据把策略写在真正使用数据的地方首页能够接受一分钟旧值代码就在路由和公开请求层明确表达// app/page.tsx export const revalidate 60 export default async function HomePage() { const [latest, categories] await Promise.all([ listArticles({ page: 1, pageSize: 10, sort: -publishedAt }), getCategoryTree(), ]) return Home initial{latest} categories{categories} / }搜索则依赖当前关键词调用方显式覆盖默认缓存exportfunctionsearchArticles(keyword:string,page1){returnserverFetchArticlePage(/articles/search,{cache:no-store,query:{keyword,page,pageSize:20},})}站点地图可以更慢更新它有自己的再验证周期// app/sitemap.tsexportconstrevalidate300exportdefaultasyncfunctionsitemap():PromiseMetadataRoute.Sitemap{constarticlesawaitlistSitemapArticles()returnarticles.map((item)({url:articleUrl(item.slug),lastModified:item.updatedAt}))}这三段代码放在一起比一句“本项目使用 ISR”准确得多。私有路由要同时控制上游与下游constupstreamawaitfetch(target,{method:request.method,headers,body,cache:no-store,})constresponseHeadersnewHeaders(upstream.headers)responseHeaders.set(cache-control,private, no-store)returnnewResponse(upstream.body,{status:upstream.status,headers:responseHeaders,})cache: no-store约束 Next.js 到上游的请求响应头约束浏览器和中间缓存。只写其中一处私有数据链路都没有闭合。路由决策矩阵路由内容所有权可接受旧值当前策略验证重点/公开60 秒ISR过期后能否更新/articles/[slug]公开正文 私有互动正文可旧、互动不可串用户服务端缓存 客户端请求两种新鲜度是否隔离/search公开但依赖关键词尽量实时动态、no-store不同 query 是否混用/member/*当前会员不可公共复用CSR 私有代理两个账号是否隔离/sitemap.xml公开300 秒较长周期再验证新文章最终是否出现用生产模式做一个最小实验pnpmbuildpnpmstartcurl-shttp://localhost:3000//tmp/home-a.htmlcurl-shttp://localhost:3000/search?keywordNext.js/tmp/search.html然后修改一篇可辨认的测试文章在 60 秒内和过期后分别保存首页响应。搜索页应按新请求计算首页则按再验证时序过渡。验证会员接口时要使用两个测试账号并检查响应中的Cache-Control只用同一个账号刷新无法证明用户之间没有缓存串读。next build会检查路由能否生成生产产物并帮助我们发现不兼容的请求时 API、服务端 import 或静态分析问题。但只看构建日志中某个路由的图标仍不足以说明它在部署平台上的全部行为。我们至少还要观察三件事首次访问时是否向上游发出请求在 TTL 内重复访问是否复用内容后台数据改变且超过 TTL 后何时出现新版本。对私有页面还要确认不同账号不会复用同一份响应。开发模式也不适合用来验证 ISR 时序。Next.js 为了让开发者立即看到修改会按需渲染页面它不能代表生产缓存。因此本项目将 Node 生产构建、OpenNext 构建和本地 Worker 运行分开验收不用next dev中的“我刷新就变了”推断线上策略。怎么为一个页面做决策我现在会按这个顺序问内容是公开的还是属于某个用户页面能否在不知道当前请求的情况下提前生成内容可以陈旧 1 分钟、5 分钟还是必须立即一致过期后可否先服务旧值再后台生成新值用户交互是整页主体还是可以拆成局部岛屿这五个问题的答案会把方案推向 SSG、ISR、动态服务端渲染或 CSR。如果只先问哪个名词更快很容易选中技术选错数据语义。适用边界本文讲的 60 秒和 300 秒只是本项目当前取舍不是内容站的通用答案。新闻站、价格页、库存页对新鲜度的要求完全不同。当内容发布后要立即全站可见时应该在发布链路增加revalidateTag或revalidatePath等按需失效机制。当部署扩展到多实例时还要解决共享缓存与失效事件传播。单机本地实验不能替代这些基础设施验证。小结我们最终没有给这个网站贴上“SSR 站点”或“ISR 站点”的标签。我们为每类数据定义了所有权、可接受旧值和获取时机再让路由和组件体现这个决策。下一篇我们会继续追问当一份公开内容设为 60 秒再验证时第 61 秒到达的用户究竟会看到什么延伸阅读App Router 与 CSR 时代的思维差异服务端组件与客户端组件数据获取与缓存如果这篇文章对你有帮助欢迎订阅我的 CSDN 专栏「成为全栈」 专栏地址https://blog.csdn.net/fungleo/category_13204651.html 本系列配套代码仓库https://github.com/fengcms/become-a-full-stack-developer