You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用renderToPipeableStream时,如何正确重水合服务端Apollo缓存?

React 18 流式SSR结合Apollo Client的缓存重水合标准方案

问题背景

我正在用React 18的renderToPipeableStream实现SSR,组件内通过Apollo Client获取数据。为了让客户端重水合服务端缓存,需要把缓存提取出来,通过服务器发送的HTML内联脚本暴露到全局window对象上。

目前我通过手动跟踪请求的方式实现了功能:

  1. 用wrapPromise包装请求,未完成时抛出Promise触发Suspense挂起
export function wrapPromise<T>(promise: Promise<T>): () => T {
  let status = 'pending';
  let response: T;
  const suspender = promise.then(
    (res) => {
      status = 'success';
      response = res;
    },
    (err) => {
      status = 'error';
      response = err;
    }
  );
  return () => {
    switch (status) {
      case 'pending':
        throw suspender;
      case 'error':
        throw response;
      default:
        return response;
    }
  };
}
  1. 编写ApolloSSRCache组件,手动遍历所有包装后的请求触发执行,待所有请求完成后渲染缓存脚本
export const ApolloSSRCache = () => {
  Object.values(renderCache).forEach((wrapped) => wrapped());
  return (
    <script
      dangerouslySetInnerHTML={{
        __html: `window.__APOLLO_STATE__=${JSON.stringify(
          client.extract()
        ).replace(/</g, '\\u003c')};`,
      }}
    />
  );
};

这个方案能正常工作,客户端useQuery可直接复用缓存,避免重复请求和页面闪烁,但手动跟踪请求的方式过于繁琐。我想知道有没有**兼容Suspense(用于代码分割)**的流式渲染下缓存暴露与重水合的标准方案?

我的更新发现

  • 更新1:如果服务端使用useSuspenseQuery,把暴露缓存的脚本标签放在渲染内容下方,缓存会被正确填充——因为执行查询的组件会自动挂起,client.extract()会在所有请求完成后调用,无需手动挂起标签。
  • 更新2:但useSuspenseQuery会忽略{fetchPolicy: 'network-only'},SSR期间会一直返回缓存数据,直到服务器重启。

标准解决方案(兼容流式SSR与Suspense)

结合Apollo Client的Suspense原生支持和React 18的流式渲染能力,推荐以下优化方案:

1. 基于useSuspenseQuery的自动缓存收集

完全利用Apollo和React的原生能力,无需手动跟踪请求:

  • 服务端组件统一使用useSuspenseQuery发起请求,组件会在请求未完成时自动触发Suspense挂起
  • 将提取缓存的脚本标签放在SSR渲染的根内容之后(比如</body>前),React流式渲染会等待所有Suspense边界内的请求完成后,再渲染该脚本标签,此时client.extract()就能拿到完整的缓存数据

示例服务端渲染逻辑:

// 服务端创建独立的Apollo Client实例(避免跨请求缓存污染)
const apolloClient = createApolloClient({ ssrMode: true });

const { pipe } = renderToPipeableStream(
  <ApolloProvider client={apolloClient}>
    <Suspense fallback={<div>加载中...</div>}>
      <App />
    </Suspense>
    {/* 放在根内容之后,自动等待所有请求完成 */}
    <script
      dangerouslySetInnerHTML={{
        __html: `window.__APOLLO_STATE__=${JSON.stringify(
          apolloClient.extract()
        ).replace(/</g, '\\u003c')};`,
      }}
    />
  </ApolloProvider>,
  {
    onShellReady() {
      // 流式输出HTML外壳
      pipe(res);
    },
    onAllReady() {
      // 所有内容渲染完成后的回调
    },
  }
);

2. 解决useSuspenseQuery忽略network-only的问题

针对SSR时useSuspenseQuery忽略fetchPolicy: 'network-only'的问题,可通过两种方式处理:

  • 每次请求创建新的Apollo Client实例:服务端处理每个请求时都初始化全新的Client,避免跨请求复用缓存,确保每次SSR都发起新的网络请求
  • 手动预取数据并写入缓存:在渲染前手动执行查询并指定network-only,将结果写入缓存后再开始渲染

示例手动预取逻辑:

// 服务端预取关键数据
const prefetchCriticalData = async (client: ApolloClient<NormalizedCacheObject>) => {
  await client.query({
    query: YOUR_CRITICAL_QUERY,
    fetchPolicy: 'network-only',
  });
};

// 渲染前先预取数据
await prefetchCriticalData(apolloClient);

// 再执行流式渲染
const { pipe } = renderToPipeableStream(/* ... */);

3. 客户端重水合逻辑

客户端初始化Apollo Client时,读取全局的window.__APOLLO_STATE__并恢复缓存:

const client = createApolloClient({
  ssrMode: false,
  cache: new InMemoryCache().restore(window.__APOLLO_STATE__),
});

ReactDOM.hydrateRoot(
  document.getElementById('root'),
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

核心优势

  • 无需手动跟踪请求/Promise,完全依赖框架原生能力
  • 完美兼容React 18流式渲染和代码分割的Suspense边界
  • 解决缓存复用问题,确保SSR每次都能获取最新数据

内容的提问来源于stack exchange,提问作者Moritz Roessler

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 14:15:24