如何在 SSR 失败时自动降级到 CSR 并返回 503
解读
国内主流前端项目普遍采用“同构渲染”:Node 层先拼好首屏 HTML,失败则把“空壳”HTML 抛给浏览器走纯 CSR,同时 SEO/监控需要明确返回 503 而非 200,避免搜索引擎误判。Grunt 作为构建层工具,本身不直接跑 Node 服务,但面试想考察的是“用 Grunt 把降级策略固化到构建与部署流程”,让开发者无法绕开。因此答题必须同时覆盖:
- Node 服务侧“SSR 异常捕获 → 回退 CSR → 设置 503”的运行时逻辑;
- Grunt 侧“构建双 Bundle、注入降级标记、生成降级映射表、CI Gate”的工程化逻辑;
- 国内云原生场景下“SLB 重试 + CDN 缓存 503 5 秒”的运维兜底。
知识点
- SSR 异常分类:渲染超时、内存溢出、后端接口 5xx、Node 进程 OOM,需分别 catch;
- res.status(503).set('Retry-After','10') 符合 RFC,国内百度/搜狗蜘蛛均识别;
- 双入口构建:Grunt 同时产出 server-bundle.js 与 client-bundle.js,并生成 manifest.json 记录哈希,用于回滚;
- Grunt 插件链:
– grunt-webpack / grunt-browserify 打双 Bundle;
– grunt-asset-manifest 生成带哈希的映射表;
– grunt-contrib-copy 把 CSR 壳模板(仅含
<div id="root"></div>与<script src="client.${hash}.js">)拷到 dist/fallback.html; – grunt-string-replace 在 Node 入口注入 process.env.SSR_FALLBACK_HTML 绝对路径,保证运维侧无法改代码即可换页; - CI Gate:grunt-fail-on-warning 在单元测试未通过时中断部署,防止“构建成功但降级逻辑未覆盖”上线;
- 监控闭环:grunt-sentry-release 上传 sourcemap,把 SSR 异常与构建版本绑定,方便国内阿里 SLS/腾讯 CLS 做链路追踪。
答案
-
Node 服务层(运行时) 在 Express 或 Koa 的 SSR 路由里统一包一层 try-catch:
import fallbackHtml from '../dist/fallback.html'; // 由 Grunt 预置 async function ssrHandler(req, res, next) { try { const html = await renderToString(<App />); return res.send(html); } catch (e) { logger.error(e); // 落盘 + 告警 res.status(503).set({ 'Retry-After': '10', 'X-Render-Mode': 'CSR-Fallback', // 方便网关统计 'Cache-Control': 'no-cache, must-revalidate' }); return res.send(fallbackHtml); } }若接口超时,可提前设置 Promise.race([render, timeout(3s)]) 主动抛错,保证 3 秒内必返回。
-
Grunt 构建层(工程化) 在 Gruntfile.js 中新增任务链:
grunt.registerTask('build:ssr', [ 'clean:dist', 'webpack:ssr', // 输出 server-bundle.js 'webpack:client', // 输出 client-${hash}.js 'asset_manifest', // 生成映射表 'copy:fallback', // 把 fallback.html 拷入 dist 'string-replace:injectPath' // 把 fallbackHtml 绝对路径写进 server.js ]); grunt.registerTask('deploy', ['test', 'build:ssr', 'sentry_release']);其中
copy:fallback保证 fallback.html 里引用的 client js 路径带哈希,避免版本漂移;string-replace把路径写死到 Node 入口,运维无需改代码即可灰度。 -
运维兜底 阿里云 SLB 侧配置“5xx 重试 1 次,间隔 500ms”,CDN 设置“503 缓存 5 秒”,防止瞬间流量把 Node 打挂;同时接入钉钉/飞书机器人,Sentry 告警 1 分钟内未恢复则自动回滚上一版本。
拓展思考
- 灰度降级:利用 Grunt 生成 manifest.json 后,把用户分桶逻辑放到网关,只让 10% 流量走新 Bundle,一旦 SSR 成功率低于 99% 则全量切换;
- 边缘渲染:国内云厂商推出 ESR(Edge Side Render),Grunt 可扩展任务把 server-bundle 直接打成 Serverless 函数 ZIP,边缘节点异常时同样返回 503 并回退到 CDN 的预渲染静态页;
- 性能预算:在 Grunt 中接入 bundle-size 插件,若 client-bundle 超过 200 KB(gzip)则中断构建,防止“降级了但 JS 太大导致 FCP 还是慢”;
- 合规场景:国内金融项目要求“交易页必须可降级到 CSR 且不可缓存”,可在 Grunt 侧为 fallback.html 自动注入 <meta http-equiv="pragma" content="no-cache"> 并走 HTTPS,满足等保测评。