按内容更新频率给页面选渲染模式:SSR / ISR / SWR / CSR 混合实践

【这是一篇写在此项目开发期的文章】 一个博客里,首页、文章详情、归档、关于、搜索、工具箱的内容更新频率完全不同,用一种渲染模式套全站总让人感到别扭。这篇文章用来记录我按"内容多久变一次"给每个路由分档、用 Nuxt 的 routeRules 配出混合方案的过程,以及在自建 node-server 上踩到的几个坑。

按内容更新频率给页面选渲染模式:SSR / ISR / SWR / CSR 混合实践

按内容更新频率给页面选渲染模式

主要问题:一种渲染模式套全站,怎么配都别扭

我的博客前台是 Nuxt 3,默认就是 SSR。一开始我图省事,想让全站都用同一种策略,结果就发现了一个问题:

  • 全站 SSR:每次请求都要在服务端渲染一遍、再打一遍后端。可归档、关于这种页面几天都不变一次,这样纯属白跑。
  • 全站 ISR / SWR:首页和文章详情是实时的——新文章、浏览量、评论,一缓存就"卡"在那一刻,读者看到的是旧页面。

所以我所遇到的情况就是:每个页面的内容更新频率不一样,应该给它们不同的模式。

解决方案:按内容更新频率给页面分档

我的判断标准

我给自己定了一个简单的判断方式:先看这个页面的内容多久变一次,再看该部分内容的更新频率是否会很影响用户体验。

  • 每次访问都必须是最新 → SSR,不配缓存规则;
  • 几分钟内旧一点没关系 → ISR / SWR,给个 TTL;
  • 内容基本不变、但依赖后台配置 → 还是 SSR(不能用构建时预渲染);
  • 纯交互、不指望被搜索引擎抓 → CSR。

按这个标准把路由过一遍,基本上每个页面的模式就确定下来了。

具体的 routeRules

Nuxt 的 routeRules 正好就是按路由指定渲染 / 缓存策略的地方。最后我配成这样(myblog-vue/myblog-blog/nuxt.config.ts):

routeRules: {
  // 欢迎落地页(/): SSR 渲染(静态落地,不缓存)
  "/": { ssr: true },
  // 主博客(/home): ISR 缓存 60 秒,过期后陈旧重验证
  "/home": { isr: 60 },
  // 归档页 ISR: 缓存 300 秒
  "/archive": { isr: 300 },
  // 分类页 ISR
  "/category": { isr: 300 },
  "/category/**": { isr: 300 },
  // 标签页 ISR
  "/tag": { isr: 300 },
  "/tag/**": { isr: 300 },
  // 关于页 SWR: 5 分钟缓存 + 10 分钟陈旧重验证
  "/about": { swr: 600 },
  // 搜索页不缓存
  "/search": { ssr: true },
  // 文章详情页 SSR(实时内容)
  "/article/**": { ssr: true },
  // 工具箱页纯客户端渲染(各页面 definePageMeta 中已设 ssr: false)
  "/tools/**": { ssr: false },
  // 上传文件代理 → 后端(backendOrigin 是 NUXT_API_BASE 去掉 /api/v1 后的根地址)
  "/uploads/**": { proxy: `${backendOrigin}/uploads/**` },
},

下面把每条想清楚。

/ 欢迎落地页:SSR,不做预渲染

这里我纠结过一阵。欢迎页几乎都是静态的——这个页面也就头像、站点名、一句简介、一个"进入博客"按钮,很适合 prerender: true,构建时就直接烤成 HTML。

但这些内容全都来自后台设置:站点名、站点 Logo、博主的头像和简介,都是运行时从后端拉的。要是构建时预渲染,等于把当时那套配置写死在 HTML 里,之后在后台改了名字、换了头像,不重新构建就看不到变化。

所以最后给了 ssr: true:每次请求服务端渲染,拿到的是最新配置,而页面本身很轻,这点开销可以接受,不过以后可以考虑一些新方案。

/home 主站:ISR 60 秒

主站是文章列表,会随发文变化,但也没必要"每来一个人就重算一次"。60 秒的窗口里旧一点完全可以接受,过期后先返回旧页面、后台再重新生成。

其实 / 原来是主站,配的是 isr: 60。这次把首页拆成 / 欢迎页 + /home 主站之后,ISR 就跟着搬到了 /home/ 改成了 SSR。

归档 / 分类 / 标签:ISR 300 秒

这几个页面的共同点是"内容由文章聚合而来",只有发新文章、改分类标签时才会变。5 分钟粒度足够,配置上就是 isr: 300(分类和标签还要加上 /** 的详情页)。

关于页:SWR 600 秒

关于页基本不变,10 分钟缓存没什么问题,用 swr: 600

/search:SSR,不配缓存规则

搜索结果和查询词强相关,是最不该被统一缓存的一类页面。我直接 ssr: true、没给它配缓存规则,每次请求都重新渲染,省得去纠结"缓存 key 里要不要带 query"。

/article/**:SSR,不配缓存规则

文章详情有浏览量和评论,这两个都是每次访问都可能变的。一旦被 ISR 缓存,浏览量就停在缓存那一刻,评论也得等缓存过期才更新。所以详情页老老实实 SSR,也没给它加缓存规则。

/tools/**:CSR

工具箱是纯前端交互——格式化、编码、解析这些,内容不依赖 SEO,也不需要服务端渲染。所以整块 ssr: false

SSR / ISR / SWR / CSR,我自己的理解

配完这一圈后,我把这些概念又理了一遍:

  • SSR(服务端渲染):每次请求,服务端把页面渲染成 HTML 返回。内容最新,代价是每次都要跑一遍渲染、打一遍后端。
  • ISR(增量静态再生成):页面按 TTL 缓存,过期后先返回旧页面、后台再生成新的。
  • SWR(stale-while-revalidate):同样是"先给旧的、后台更新",在 Nuxt 的 routeRules 里和 ISR 用法几乎一样,只是语义上更偏"缓存"。
  • CSR(客户端渲染):服务端只发一个壳,内容全在浏览器里渲染。交互方便,但首屏要等 JS,SEO 也吃亏。

Nitro 的文档里 routeRules 现在还标着 experimental,不过 Nuxt 这边用起来已经很顺手了,我暂时没因为这个遇到什么坑。

踩到的几个坑

自建 node-server 上,isrswr 其实是一回事

这是我配完才反应过来的。文档写得很清楚:isrswr 行为相同,区别在于 isr 能额外把响应交给 CDN 缓存——但那是 Netlify / Vercel 这类平台才有的能力。

我是选择 docker 自托管,跑的是 Nitro 默认的 node_server preset,没有那层 CDN。所以对我来说 isr: 300swr: 300 跑起来没什么区别,isr 更像是"语义上表达我想做什么"。以后要是换到支持 CDN 的平台,这层区别会真正体现出来。

这些缓存是内存里的,重启就没了

ISR / SWR 的缓存存在 Nitro 的 storage 里,我这边没配外部存储,默认就是内存。也就是说容器一重启、或者重新部署,缓存全清,第一波访问得重新生成。

对博客这点访问量影响不大,但得记得它不是"持久化缓存",不能拿它当兜底。

同一套规则写了两遍

/tools/** 我在 nuxt.config.tsrouteRules 里写了 ssr: false,工具箱的每个页面里又各自写了 definePageMeta({ ssr: false })。这其实是重复的,留一处就行,我还没清理,先在这里记一笔。

routeRules 不只是"渲染模式"

顺带提一句:这块配置里我还放了 /uploads/** 的代理规则,把上传目录转发到后端。它跟渲染模式没关系,但确实写在同一个 routeRules 里——说明它本质上是"按路由改行为",渲染只是其中一类。

还没想清楚的地方

  1. 工具箱页是纯 CSR,首屏要等 JS 下载执行,会先白一下。虽然加载模板能缓解白屏,但我不太喜欢那种过渡,先记着吧。

另外 ISR / SWR 的缓存现在是内存的,我在想是不是该接个 Redis,让重启之后不用从头再生成一遍。不过按博客这点访问量,现在做可能有点过度设计,先记着,等真的觉得慢再说。

浙ICP备2026077668号

欢迎来到我的网站 这个网站主要是作技术和生活两个维度的记录