面试官:遇到 SSR 白屏、请求混乱怎么处理 ❓❓❓

482 阅读8分钟

最近在出一个前端的体系课程,里面的内容非常详细,如果你感兴趣,可以加我 v 进行联系 yunmz777:

image.png

浪费你几秒钟时间,内容正式开始

在前端项目中,SSR(服务端渲染)能够提升首屏速度和 SEO 体验,但也带来了一类典型问题:页面白屏与请求异常。这类问题通常难以第一时间判断究竟是服务端渲染失败,还是客户端激活阶段出错,更可能夹杂接口重复、跨域、协议不一致等请求混乱。由于 SSR 涉及前后端协作,问题一旦出现往往影响整页用户体验。为了快速定位和止损,可以遵循以下分步骤排查流程,从环境、源码、客户端表现到服务端日志逐层缩小范围,帮助工程师高效定位问题根源。

SSR 白屏如何排查

1. 还原线上环境

首先要尽可能还原出线上真实的运行环境,这一步是排查的起点。请确保使用生产构建启动应用(例如在 Node.js 中设置 NODE_ENV=production),然后访问和线上完全一致的域名、路径。浏览器端则需要清理缓存、关闭可能干扰的扩展,并尽量使用隐身模式来访问页面。这样可以避免缓存残留或开发环境配置差异带来的干扰,从而获得最接近真实用户的行为表现。

2. 查看页面源码

接下来要确认 SSR 阶段是否正常输出 HTML。不要只看浏览器 DevTools 的 Elements,而是直接使用“查看页面源代码”。如果源代码是空的或者结构异常,说明 SSR 渲染阶段可能报错或者模板注入失败。反之,如果源码正常输出了完整的 HTML,但屏幕仍然是白的,那就很可能是客户端 Hydration 失败或首屏 JS 出错。这一步能快速帮你区分问题是在 SSR 进程端,还是发生在客户端激活阶段。

3. 检查控制台与网络请求

如果 SSR 输出正常,就要继续观察客户端执行情况。打开浏览器的 Console 和 Network 面板。

  • 在 Console 中,如果出现 hydration mismatch 或类似 Cannot read property 'xxx' of undefined 的错误,那就是典型的客户端执行阶段异常,通常是前后端渲染不一致引发的。

  • 在 Network 中,如果你发现同一个接口被重复请求,或者有跨域 401/403 报错,又或者出现 http/https 协议混用,那就是请求逻辑或配置上的问题。这类问题通常会导致“请求混乱”,即请求的数量、顺序或者目标都与预期不符。

4. 禁用 JavaScript 验证

为了进一步确认问题的根源,可以在浏览器 DevTools 的 Command Menu 中使用 Disable JavaScript。当 JS 被禁用后,如果页面仍能显示 SSR 渲染的内容,说明 SSR 是正常的,只是客户端激活(Hydration)时出了问题。而如果禁用 JS 后依旧是空白,说明 SSR 渲染本身就失败了。这一步能快速区分是 SSR 失败 还是 客户端激活失败。

5. 检查服务端日志

最后要结合服务端日志和监控平台(APM 或 Sentry)进行确认。如果 SSR 渲染出错,服务端日志里通常会有堆栈信息。如果错误是接口请求超时或者未捕获异常,可以先给 SSR 渲染加一层兜底逻辑,比如设置合理的超时和降级策略,避免整页空白。与此同时,还可以利用 Sentry 或 Datadog 等工具,捕捉 SSR 阶段和客户端阶段的异常,帮助后续进一步定位和修复问题。

6. 流程总结

根据上面的流程,我们可以整理出如下流程:

20250827092227

如何解决

不同的原因导致的白屏,有不同的解决方案。

A. Hydration 失败 —— 白屏的头号元凶

当你发现 查看页面源代码时 HTML 是完整的,但浏览器激活后页面却消失,并且控制台报错 hydration mismatchchecksum error,那么问题很可能出在 Hydration 阶段。最常见的原因是 SSR 和 CSR 渲染结果不一致。例如,如果你在服务端直接调用 Date.now()Math.random(),SSR 输出和客户端计算的结果就会不同,最终触发白屏。

错误示例:

// SSR 与 CSR 输出不同
export default function Time() {
  return <div>{Date.now()}</div>;
}

正确做法是让 SSR 输出稳定占位,实际值在客户端挂载后再更新:

// SSR 阶段输出一致,CSR 挂载后更新
import { useEffect, useState } from "react";
export default function Time() {
  const [time, setTime] = useState(0);
  useEffect(() => setTime(Date.now()), []);
  return <div>{time}</div>;
}

另一个常见场景是组件依赖 windowdocument,无法在 SSR 运行。如果硬渲染,就会导致激活失败。此时需要明确标记为“仅客户端组件”。在 Next.js 里可以这样写:

import dynamic from "next/dynamic";
const Chart = dynamic(() => import("./Chart"), { ssr: false });

而在 Nuxt 中则可以用 <client-only> 包裹。

B. 请求混乱 —— 重复、错乱、不一致

如果发现接口请求数量比预期多,或者 SSR 阶段和 CSR 阶段打到的 API 不一样,那就是请求混乱。最典型的情况是 SSR 请求了一次数据,客户端激活时又重复请求了一次。解决方法是 SSR 阶段将数据注入到全局变量,客户端直接使用它,而不是再打一遍接口:

// SSR 注水
return { props: { initialData: data } };

// CSR 复用
const { data } = useSWR("/api/post", fetcher, {
  fallbackData: initialData,
  revalidateOnMount: false,
});

还有一种情况是 SSR 阶段请求环境不对,比如直接调了 http://localhost,或者没有带上用户 Cookie,导致 SSR 报 401,但 CSR 却正常。这时需要确保 SSR 请求用的是配置好的 baseURL,并把请求头透传:

// Axios 示例
const axiosSSR = axios.create({ baseURL: process.env.API_ORIGIN });
axiosSSR.defaults.headers.Cookie = req.headers.cookie || "";

// fetch 示例
await fetch(new URL("/api/user", process.env.API_ORIGIN), {
  headers: { cookie: req.headers.cookie },
});

此外,如果页面依赖多个并发请求,返回顺序不一致也可能导致覆盖问题。解决方案是引入版本号或时间戳,确保只接受最新的响应:

if (newData.version >= state.version) {
  setState(newData);
}

C. SSR 进程错误或超时 —— 源码为空

如果你在浏览器里查看页面源代码,结果 HTML 本身就是空的或只有一个壳子,那问题就不在 Hydration,而是 SSR 渲染过程直接失败了。常见的原因是 SSR 抛出未捕获的异常,或者接口阻塞时间太长。

解决思路有三步:首先,加错误边界,保证 SSR 崩溃时至少能返回一个兜底 HTML。例如在 React 中:

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  render() {
    return this.state.hasError ? <div>渲染失败</div> : this.props.children;
  }
}

其次,为 SSR 渲染加超时,避免页面一直挂起:

const renderWithTimeout = async () => {
  return Promise.race([
    renderToString(<App />),
    new Promise((_, rej) => setTimeout(() => rej("timeout"), 2000)),
  ]);
};

最后,对于关键接口,可以引入服务端缓存或 CDN 缓存,减少 SSR 阻塞风险:

const cache = new Map();
async function fetchWithCache(url: string) {
  if (cache.has(url)) return cache.get(url);
  const res = await fetch(url).then((r) => r.json());
  cache.set(url, res);
  return res;
}

下面是在不改变原意的前提下的精炼版,把说明改成更连贯的文字,并对代码做了小幅增强(健壮性/可读性更好)。


最小示例:避免双请求 + 仅客户端组件

当页面需要 SSR 首帧数据时,先在服务端取数并注水到页面 props,客户端用 SWR 以相同 key 复用这份数据,设置 revalidateOnMount: false 避免激活时再打一枪;对于依赖浏览器 API 的组件,标记为仅客户端渲染,杜绝在 SSR 阶段访问 window/document 导致 hydration 失败。

注水与去重(React/Next.js)

// pages/[slug].tsx
import type { GetServerSideProps } from "next";
import useSWR from "swr";

const fetcher = (url: string, cookie?: string) =>
  fetch(url, { headers: cookie ? { cookie } : undefined }).then((r) =>
    r.json()
  );

type Post = { id: string; title: string; content: string };
type Props = { initialData: Post };

export const getServerSideProps: GetServerSideProps<Props> = async (ctx) => {
  const url = new URL(
    `/post/${ctx.params!.slug}`,
    process.env.API_ORIGIN!
  ).toString();
  const data = await fetcher(url, ctx.req.headers.cookie);
  return { props: { initialData: data } };
};

export default function Page({ initialData }: Props) {
  // 复用 SSR 数据作为首帧,避免 CSR 重复请求;激活后按需再校验
  const { data } = useSWR<Post>(
    `/post/${initialData.id}`,
    (key) =>
      fetcher(new URL(key, process.env.NEXT_PUBLIC_API_ORIGIN!).toString()),
    {
      fallbackData: initialData,
      revalidateOnMount: false,
      dedupingInterval: 2000,
    }
  );
  return <PostView data={data!} />;
}

function PostView({ data }: { data: Post }) {
  return (
    <article>
      <h1>{data.title}</h1>
      <p>{data.content}</p>
    </article>
  );
}

仅客户端组件(避免 SSR 阶段使用浏览器 API)

"use client";

import { useEffect, useState } from "react";

export default function ChartClient() {
  const [width, setWidth] = useState<number | null>(null);

  useEffect(() => {
    // SSR 无 window;挂载后再读
    const update = () => setWidth(window.innerWidth);
    update();
    window.addEventListener("resize", update, { passive: true });
    return () => window.removeEventListener("resize", update);
  }, []);

  return <div>chart width: {width ?? "measuring..."}</div>;
}

20250827094449

生产防线(优化版)

要减少线上翻车,建议在工程层面“预埋栏杆”:

  1. 编码规范禁止 SSR 路径直接访问 window/document,所有非确定性渲染(时间、随机数、窗口尺寸、本地存储)一律转移到 useEffect/onMounted

  2. 数据层约定统一走 SSR 取数与注水,客户端以注水为初值并默认不重复请求首帧;

  3. 可观测性同时接入 Sentry/Datadog,分别上报 SSR 渲染异常与浏览器运行时错误,并为 hydration-mismatch 单独打点;

  4. 回退机制则在 SSR 入口加错误边界与渲染超时,超时/异常时返回降级 HTML 交由客户端接管,确保“即便失败也不白屏”。

总结

在 SSR 白屏问题中,首要的是区分 SSR 渲染失败 和 客户端 Hydration 失败:前者通常会导致源码直接为空,后者则表现为源码正常但页面激活崩溃。Hydration 问题多由前后端渲染不一致引起,需要将非确定性逻辑(如 Date.now()、window API)延迟到客户端执行,并对依赖浏览器的组件标记为仅客户端渲染。请求混乱则应通过 SSR 注水 + 客户端复用数据来避免重复调用,同时确保 baseURL、Cookie 透传一致。最后,SSR 渲染时要增加错误边界、超时和缓存策略,确保即使渲染失败也能返回降级 HTML,从而避免整页白屏。