首页
/ tRPC v10 服务端渲染(SSR)实战指南:基于 Next.js Pages Router 的 `ssr: true` 配置、请求头转发与响应缓存

tRPC v10 服务端渲染(SSR)实战指南:基于 Next.js Pages Router 的 `ssr: true` 配置、请求头转发与响应缓存

2026-09-08 09:18:08作者:何将鹤

本文档对应仓库中的 version-10.x SSR 指南,是一份面向 tRPC v10 + Next.js Pages Router 的端到端 SSR 接入手册。

本篇技术指南以 tRPC v10 文档中的 SSR 章节为核心,系统讲解如何在 Next.js Pages Router 应用中通过 @trpc/nextcreateTRPCNext 开启服务端渲染:从最简的 ssr: true 一行开关、浏览器/服务端双环境 config 拆分、cookie 请求头转发、条件式 SSR 判断,到 responseMeta 响应缓存,以及与 getServerSideProps/getStaticProps 混用时的取舍。读完本文,你将能独立为 tRPC 10 应用配置出数据在服务端完成预取(prefetch)、客户端零首屏空载的 SSR 链路,并理解其背后 getInitialProps 与 hydration 的工作机制。

关联文档与适用版本说明

本文的主体内容是仓库中 ssr.md(位于 www/versioned_docs/version-10.x/client/nextjs/ 目录,与 setup.mdxserver-side-helpers.mdssg.md 同级)。该文档针对 tRPC v10 的 Pages Router 用法,代码基于 pages/ 目录 + getInitialProps/getServerSideProps/getStaticProps。同一主题在 v11 文档中被迁移至 www/docs/client/nextjs/pages-router/ssr.md,二者的核心概念一致,但 v11 需要额外传入 ssrPrepass(本文「底层原理」一节会结合仓库源码解释这一演进)。

开启 SSR:从一行 ssr: true 开始

要让 tRPC 查询在 Next.js 服务端渲染阶段被执行,只需要在 createTRPCNext 的配置对象中设置 ssr: true

import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import superjson from 'superjson';
import type { AppRouter } from './api/trpc/[trpc]';

export const trpc = createTRPCNext<AppRouter>({
  config(config) {
    // ...
  },
  ssr: true,
});

关键点在于:

  • 默认值是关闭的ssr 不传即为 false)。在默认情况下,所有查询只会在浏览器端通过 useQuery 触发 HTTP 请求。
  • 开启后,tRPC 会利用 getInitialProps 在服务端预取(prefetch)所有查询,把结果随页面首屏 HTML 一并下发,客户端渲染时直接读取已就绪的数据,不再出现“先 loading、再请求”的空窗。

:::info 官方文档特别提醒一个版本性约束:开启 SSR 后 tRPC 依赖 getInitialProps 完成服务端预取,这与 Next.js 的 getServerSideProps 一起使用会产生已知问题(tRPC 仓库 issue #596 一类的报错,例如 getServerSideProps 会接管页面数据流、干扰 getInitialProps 注入的 trpcState),而该问题并不在 tRPC 的可解决范围内。因此:

让查询在服务端正确执行:拆分浏览器与服务端 config

仅仅打开开关还不够。createTRPCNextconfig(config) 回调在服务端渲染与浏览器端都会被调用,而两者运行环境截然不同,因此文档要求在其中补充额外逻辑——通常写法是typeof window !== 'undefined' 做环境判断,分别返回两套链接配置

import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import superjson from 'superjson';
import type { AppRouter } from './api/trpc/[trpc]';

export const trpc = createTRPCNext<AppRouter>({
  config(config) {
    const { ctx } = opts; // NextPageContext,仅服务端存在 req/res
    if (typeof window !== 'undefined') {
      // during client requests
      return {
        transformer: superjson, // optional - adds superjson serialization
        links: [
          httpBatchLink({
            url: '/api/trpc',
          }),
        ],
      };
    }

    return {
      transformer: superjson, // optional - adds superjson serialization
      links: [
        httpBatchLink({
          // The server needs to know your app's full url
          url: `${getBaseUrl()}/api/trpc`,
          /**
           * Set custom request headers on every request from tRPC
           */
          headers() {
            if (!ctx?.req?.headers) {
              return {};
            }
            // To use SSR properly, you need to forward client headers to the server
            // This is so you can pass through things like cookies when we're server-side rendering
            return {
              cookie: ctx.req.headers.cookie,
            };
          },
        }),
      ],
    };
  },
  ssr: true,
});

这段代码里有四个必须理解的细节:

  1. 浏览器端使用相对路径 /api/trpc:同源下由浏览器直接发起请求,无需知道应用全量 URL。
  2. 服务端必须使用全量 URL:SSR 阶段的数据请求是由 Node.js 服务端发起的 HTTP 调用(而非浏览器),因此要用 ${getBaseUrl()}/api/trpc 拼出应用的完整地址(如 http://localhost:3000/api/trpc)。getBaseUrl() 是你自己维护的辅助函数,典型实现是“浏览器端返回空字符串(走相对路径),服务端返回部署后的绝对地址”。
  3. headers() 回调转发 cookie 等客户端头config 回调接收的参数(形参名可以叫 config/ctx/opts,本文与文档代码里混用了命名)内含 ctx,即 Next.js 的 NextPageContext;只有在服务端它才携带 ctx.req.headers。把 req.headers.cookie 原样转发出去,才能让后端拿到与浏览器一致的登录态/会话信息——否则 SSR 渲染出的页面会因为“没有 cookie”而拿到与客户端不同的数据。若拿不到 ctx.req.headers(例如某些上下文缺失的场景),则返回空对象兜底。
  4. transformer: superjson 为可选项:用于序列化 DateMap 等 JS 特有类型,需要保证客户端与服务端一致。

为什么不能只靠相对路径 + 浏览器头?

原因就在第 2、3 点:服务端渲染发生在 Node 进程里,此处“客户端”是 Node 的 fetch 实现,既不认识浏览器的 location,也不会自动携带用户的 cookie。所以这两段服务端专属逻辑是 SSR 能否“渲染出正确数据”的分水岭。

条件式 SSR:只对特定请求开启预取

如果不想对所有请求都做 SSR,可以把 ssr 从布尔值改成一个回调函数。该回调会在服务端被调用,接收 opts(内含 opts.ctx),既可以同步返回布尔值,也可以返回一个 resolve 为布尔值的 Promise:

import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import superjson from 'superjson';
import type { AppRouter } from './api/trpc/[trpc]';

export const trpc = createTRPCNext<AppRouter>({
  config(config) {
    const { ctx } = opts;
    if (typeof window !== 'undefined') {
      // during client requests
      return {
        transformer: superjson, // optional - adds superjson serialization
        links: [
          httpBatchLink({
            url: '/api/trpc',
          }),
        ],
      };
    }

    return {
      transformer: superjson, // optional - adds superjson serialization
      links: [
        httpBatchLink({
          url: `${getBaseUrl()}/api/trpc`,
          headers() {
            if (!ctx?.req?.headers) {
              return {};
            }
            return {
              cookie: ctx.req.headers.cookie,
            };
          },
        }),
      ],
    };
  },
  ssr(opts) {
    // only SSR if the request is coming from a bot
    return opts.ctx?.req?.headers['user-agent']?.includes('bot');
  },
});

文档给出的示例是一个常见的 SEO 场景:只对爬虫/机器人请求做服务端渲染,从而让搜索引擎拿到完整 HTML,而普通用户在客户端享受 SPA 的交互体验——这是对性能和 SEO 的一种经典折中。请注意:从仓库当前源码的类型定义可以推断,该回调的签名是 (opts: { ctx: NextPageContext }) => boolean | Promise<boolean>,其中 opts.ctx 即 Next.js 的页面上下文,因此可以读取 user-agentcookie 等任意请求头作为判定依据(见 withTRPC.tsxWithTRPCSSROptions.ssr 的类型声明)。由于回调允许返回 Promise,你甚至可以在其中做异步判断(例如调用某个鉴权/设备识别服务)。

_app.tsx 中挂载 Provider:trpc.withTRPC

无论 SSR 是否开启、是否条件化,客户端都需要一个顶层 Provider 注入 QueryClient 与 tRPC 客户端,这一步通过 createTRPCNext 暴露的 withTRPC 高阶组件完成:

import { trpc } from '~/utils/trpc';
import type { AppProps } from 'next/app';
import React from 'react';

const MyApp: AppType = ({ Component, pageProps }: AppProps) => {
  return <Component {...pageProps} />;
};

export default trpc.withTRPC(MyApp);

要点:

  • withTRPC 包裹的是 Next.js 的 App 根组件,它内部会创建 QueryClientProvidertrpc.Provider 以及基于 pageProps.trpcStateHydrationBoundary(该机制在 v11 仓库源码中可见,见下文「底层原理」)。
  • SSR 开启时,withTRPC 会接管 getInitialProps:先执行应用自身的数据获取逻辑,再运行服务端预取,最终把脱水后的 trpcState 合并进 pageProps,保证客户端拿到同构的初始数据。

结合仓库源码看底层原理(v10 机制与 v11 的 ssrPrepass 演进)

版本化文档描述的 v10 行为是:ssr: true 时 tRPC 自动经由 getInitialProps 在服务端渲染前预取全部查询,无需额外传参。而在本仓库当前的 @trpc/next 实现(面向 v11)中,这一逻辑被重构为显式的 ssrPrepass 机制,恰好可以作为理解 v10 内部细节的“放大镜”:

  • withTRPC.tsx 定义了 WithTRPCSSROptions:开启 SSR 时除了 ssr: true | ((opts) => boolean | Promise<boolean>),还必须提供 ssrPrepass 与可选的 responseMeta
  • ssrPrepass.ts 展示了完整流水线:在服务端调用 parent.config({ ctx }) 创建 tRPC 客户端与 QueryClient,然后react-dom/serverrenderToString 循环渲染组件树——只要一次渲染引发了新的查询抓取(queryClient.isFetching() 为真)就继续下一轮,直至数据全部稳定,从而天然支持 HTTP 批量请求(batching)的逐轮结算;最终用 dehydrate 导出缓存并序列化为 pageProps['trpcState'] 传给客户端。
  • 渲染期间 ssrState 被标记为 prepass(见 ssrPrepass.ts);tRPC 客户端内部用 SSRState = 'mounted' | 'mounting' | 'prepass' | false 来区分渲染阶段并决定是否发起客户端请求(见 context.tsx)。
  • 脱水出的错误会被规范化为 TRPCClientErrorLike 对象再序列化(ssrPrepass.ts),避免 Error 实例无法跨环境传输的问题。

一句话总结:v10 用 getInitialProps 自动预取,v11 用 ssrPrepass 的“预渲染 + 循环脱水”完成同样目标。阅读 v10 部署代码时若发现 TRPCClientError: fetch failedtrpcState 缺失,多半需要回到「双环境 config 与请求头转发」检查。

为 SSR 页面叠加响应缓存:responseMetacache-control

SSR 会显著增加每个请求的渲染开销。文档建议 SSR 开启后结合 服务端响应缓存 使用:给 SSR 产出的 HTML 响应加上 cache-control 头,让 CDN/边缘网络兜住重复流量(对公开、不含个人信息的路由尤其有效)。

缓存逻辑通过 createTRPCNextresponseMeta 回调注入——它会在 SSR 结束时被调用,接收 { ctx, clientErrors },返回 { status?, headers? }。下面是从该缓存文档中摘出的典型用法(v10 版本):

import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { AppRouter } from '../server/routers/_app';

export const trpc = createTRPCNext<AppRouter>({
  config(config) {
    if (typeof window !== 'undefined') {
      return {
        links: [
          httpBatchLink({
            url: '/api/trpc',
          }),
        ],
      };
    }

    const url = process.env.VERCEL_URL
      ? `https://${process.env.VERCEL_URL}/api/trpc`
      : 'http://localhost:3000/api/trpc';

    return {
      links: {
        http: httpBatchLink({
          url,
        }),
      },
    };
  },
  ssr: true,
  responseMeta(opts) {
    const { clientErrors } = opts;

    if (clientErrors.length) {
      // propagate http first error from API calls
      return {
        status: clientErrors[0].data?.httpStatus ?? 500,
      };
    }

    // cache request for 1 day + revalidate once every second
    const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
    return {
      headers: {
        'cache-control': `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
      },
    };
  },
});

这里的要点:

  • 有错误时不缓存并透传状态码:遍历 clientErrors,取第一个错误的 HTTP 状态码(缺省回退 500),避免把出错页面缓存成“正常页”。
  • 成功时下发 s-maxage=1, stale-while-revalidate=86400:CDN 层缓存 1 秒后进入“后台异步重新验证”的 stale-while-revalidate 模式,配合 SSG 思路可以让整个应用接近静态站点速度。
  • 服务端地址仍可用 process.env.VERCEL_URL 之类的部署环境变量动态拼出,而非硬编码。

:::warning 缓存前务必确认响应不含个人信息。tRPC 默认启用批量请求(batching),一个 HTTP 响应可能同时携带多条查询结果;如果其中混入鉴权数据,建议在有 cookie/auth 头时跳过缓存,或用 splitLink 把公共查询与私有查询拆到不同链路。相关讨论详见 服务端响应缓存。 :::

FAQ:SSR 接入中的高频问题

文档末尾整理了三个几乎人人都会踩的坑,这里完整保留并补充实现层面的解读。

Q1:为什么必须手动把客户端请求头转发给服务端?tRPC 不能自动做吗?

虽然绝大多数 SSR 场景都想转发客户端头,但服务端链路上你常常需要在转发的基础上动态增删头(例如临时附加某个上游 token、改写 header 键避免冲突)。tRPC 不愿意替你承担“header 键冲突、覆盖顺序”这类责任,所以把转发策略完全交给你在 headers() 中控制——这也正是上述 config 里单独为服务端写 cookie 转发的原因。

Q2:在 Node 18 上做 SSR 时,为什么需要删除 connection 请求头?

如果不移除 connection 头,数据抓取会以 TRPCClientError: fetch failed 失败。原因在于 connection 属于 HTTP 规范中的 forbidden header name(禁用头名),浏览器/符合规范的 fetch 实现禁止手动设置它。当你在服务端从 Node 发起请求却把旧头原样带上时,便会触发该错误;标准的解法是在服务端构建请求头前先剔除 connection

Q3:明明 SSR 已经返回了初始数据,为什么 Network 面板里还能看到请求?

因为 @tanstack/react-query(tRPC 数据抓取 Hook 的底层库)默认会在组件挂载(mount)与窗口重新聚焦(window refocus)时重新抓取数据,即使它已经通过 SSR 拿到了初始数据——这是为了保证数据始终新鲜,并非 SSR 失效。若你的页面数据基本静态、希望完全避免重复请求,可关闭该行为:参考 SSG 静态生成指南 中的做法,在查询选项里设置 refetchOnMount: falserefetchOnWindowFocus: false,或在 createTRPCNextqueryClientConfig.defaultOptions.queries 中全局关闭。

小结:v10 SSR 的决策树

  • 需要全站/多数页面 SSR,且页面走 getInitialProps → 直接 ssr: true,并在 config 里实现“双环境链接 + cookie 转发”;
  • 只想对爬虫等特定请求 SSR → ssr 传回调,按 ctx.req.headers['user-agent'] 等判定;
  • 页面使用 getServerSideProps / getStaticProps不要开 ssr: true,改用 Server-Side Helpers 预取并在 props 里返回 trpcState: helpers.dehydrate()
  • 追求极致首屏速度、内容公开无隐私 → 叠加 responseMeta 下发 cache-control,参考 响应缓存指南
  • SSR 与数据抓取行为相关的更多说明,可继续阅读 SSGServer-Side Helpers;同时仓库示例目录 examples/ 中提供了大量可运行的 Next.js + tRPC 项目供对照。
热门项目推荐
相关项目推荐

项目优选

收起
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.15 K
2.77 K
kernelkernel
deepin linux kernel
C
34
18
docsdocs
暂无描述
Markdown
900
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
929
1.85 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.36 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.94 K
1.03 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.47 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
534
603
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
398
leetcodeleetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
Markdown
77
23