如何在 SSR 失败时自动降级到 CSR 并返回 503

解读

国内主流前端项目普遍采用“同构渲染”:Node 层先拼好首屏 HTML,失败则把“空壳”HTML 抛给浏览器走纯 CSR,同时 SEO/监控需要明确返回 503 而非 200,避免搜索引擎误判。Grunt 作为构建层工具,本身不直接跑 Node 服务,但面试想考察的是“用 Grunt 把降级策略固化到构建与部署流程”,让开发者无法绕开。因此答题必须同时覆盖:

  1. Node 服务侧“SSR 异常捕获 → 回退 CSR → 设置 503”的运行时逻辑
  2. Grunt 侧“构建双 Bundle、注入降级标记、生成降级映射表、CI Gate”的工程化逻辑
  3. 国内云原生场景下“SLB 重试 + CDN 缓存 503 5 秒”的运维兜底

知识点

  • SSR 异常分类:渲染超时、内存溢出、后端接口 5xx、Node 进程 OOM,需分别 catch;
  • res.status(503).set('Retry-After','10') 符合 RFC,国内百度/搜狗蜘蛛均识别;
  • 双入口构建:Grunt 同时产出 server-bundle.jsclient-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 做链路追踪。

答案

  1. 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 秒内必返回。

  2. 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 入口,运维无需改代码即可灰度。

  3. 运维兜底 阿里云 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,满足等保测评。