首页
/ React Router 预渲染(Pre-Rendering)深度指南:构建期渲染配置、并发控制与 SSR/SPA 双模式实战

React Router 预渲染(Pre-Rendering)深度指南:构建期渲染配置、并发控制与 SSR/SPA 双模式实战

2026-09-07 21:54:01作者:凌朦慧Richard

Pre-Rendering(预渲染)是 React Router Framework 模式提供的构建期能力:在 react-router build 阶段就把静态内容渲染成 HTML 与数据文件,替代运行时再逐请求渲染,从而显著降低页面加载延迟与服务器负载。本文以官方文档 docs/how-to/pre-rendering.md 为核心,结合 packages/react-router-dev 的配置定义、Vite 插件与构建源码,系统讲解 prerender 的四种配置形态、并发参数、输出文件布局,以及配合 ssr: true/false 在“运行时 SSR 服务器”与“纯静态服务器”两种部署形态下的完整落地方式。读完你将能够在自己的 Framework 应用中独立完成静态路径、动态路径(如 /blog/:slug)、SPA 降级与静态站点(SSG)等多种预渲染方案。

预渲染解决什么问题

传统 SSR 应用中,浏览器每次访问一个页面,都要先请求服务器、触发对应路由的 loader、再服务端渲染出 HTML 返回。对于内容变化不频繁的页面(博客文章、营销页、文档站),这种“运行时重复劳动”完全没有必要。

预渲染的思路是:把渲染时机从运行时提前到构建时。构建阶段直接生成静态 HTML(以及客户端导航所需的数据文件)写入构建产物目录,部署后由 CDN 或静态文件服务器直接分发,省掉中间一整条请求处理链路。

需要明确的是,该能力仅在 Framework 模式 下可用(原文档以 [MODES: framework] 标注)。它在当前仓库中的完整实现位于 packages/react-router-dev/vite/plugin.tspackages/react-router-dev/vite/plugins/prerender.ts

Configuration:四种 prerender 配置形态

预渲染由 react-router.config.ts 中的 prerender 配置项开启。其类型定义在 config/config.ts

export type PrerenderPaths =
  | boolean
  | Array<string>
  | ((args: {
      getStaticPaths: () => string[];
    }) => Array<string> | Promise<Array<string>>);

即一个 prerender 字段可以承载四种写法:布尔值、字符串路径数组、返回路径数组的(可异步)函数,以及携带 concurrency 的对象。下面逐一展开。

布尔值 true:预渲染全部静态路径

最简单的配置是布尔 true,它会自动预渲染应用的全部静态路径:

import type { Config } from "@react-router/dev/config";

export default {
  prerender: true,
} satisfies Config;

注意:布尔 true 不会包含任何动态路径(如 /blog/:slug),因为参数取值未知。构建器是如何判定“静态路径”的呢?从源码看,plugin.ts 中的 getStaticPrerenderPaths 会从 "/" 开始递归遍历 routes.ts 生成的整棵路由树,逐段拼接路径;只要某个路径段以 : 开头(动态段)或是 *(splat 段),就把整条路径归入 paramRoutes(参数化路径),其余才计入可预渲染的 paths

if (segments.some((s) => s.startsWith(":") || s === "*")) {
  paramRoutes.push(route.path);
} else {
  paths.push(newPath);
}

因此 /blog/:slug/docs/* 这类带参数或通配的路由天然不会被 true 覆盖。而最终 getPrerenderPaths 会对路径做“清理双斜杠、去除末尾斜杠”的处理(见 plugin.ts),保证输出路径规整。

字符串数组:精确声明含动态值的路径

需要预渲染带动态参数的路径时,改为提供路径数组,把具体参数值展开成最终 URL:

import type { Config } from "@react-router/dev/config";

let slugs = getPostSlugs();

export default {
  prerender: [
    "/",
    "/blog",
    ...slugs.map((s) => `/blog/${s}`),
  ],
} satisfies Config;

getPostSlugs() 可以是任何同步逻辑(从 CMS、本地文件系统或硬编码列表读取)。这种方式把“有哪些动态路径”的决定权完全交给你的数据源。

函数形式:异步获取 + getStaticPaths

如果路径集合需要更复杂、或异步地从 CMS/数据库拉取(例如构建前请求远端服务),可提供一个返回路径数组的函数。函数参数会注入一个 getStaticPaths() 方法,用于免去手动罗列应用中全部静态路径的麻烦:

import type { Config } from "@react-router/dev/config";

export default {
  async prerender({ getStaticPaths }) {
    let slugs = await getPostSlugsFromCMS();
    return [
      ...getStaticPaths(), // "/" and "/blog"
      ...slugs.map((s) => `/blog/${s}`),
    ];
  },
} satisfies Config;

其实现逻辑位于 plugin.ts:当配置是函数时,构建器把 getStaticPaths: () => getStaticPrerenderPaths(...).paths 作为实参注入——与 prerender: true 走的正是同一套路由树遍历逻辑,因此二者产出的静态路径集合保持一致,函数形式只是在其之上叠加了异步数据源。

配置合法性在 config/config.ts 有运行时校验:prerender/prerender.paths 必须是布尔值、字符串数组或函数;prerender.concurrency 若指定必须是正整数。仓库还保留了旧的 unstable_concurrency 字段提示,引导用户迁移到已稳定的 prerender.concurrency

Concurrency:并发预渲染加速构建

默认情况下,页面一个路径接一个路径串行预渲染。由于每次预渲染都要经过完整的请求处理链路(启动 preview server、匹配路由、跑 loader、渲染 HTML),当页面数量多时串行会拖慢构建。可以配置并发让多个路径并行渲染:

import type { Config } from "@react-router/dev/config";

let slugs = getPostSlugs();

export default {
  prerender: {
    paths: [
      "/",
      "/blog",
      ...slugs.map((s) => `/blog/${s}`),
    ],
    concurrency: 4,
  },
} satisfies Config;

prerender 从数组/布尔升级为 { paths, concurrency } 对象后即可指定并发度。原文档特别提醒:并发数值需要结合你的应用实验取舍,通常能加快构建,但更高的并发也意味着更高的资源消耗,并向服务器/CMS 发出更多并发请求(配置注释在 config/config.ts 有同样说明)。若省略该字段,默认值是 1(完全串行)。

并发如何落到实处?构建器把路径集合交给 packages/react-router-dev/vite/plugins/prerender.ts 中的通用 prerender 插件,插件内部通过 p-map{ concurrency } 并发驱动所有请求(见 plugins/prerender.ts)。该插件还内置了一批可调参数:timeout(单请求超时,默认 10000ms)、retryCount(失败重试次数,默认 0,只重试 5xx 与超时、不重试 4xx)、retryDelay(默认 500ms)、maxRedirects(默认 0)等,具体见 PrerenderConfig 接口

两种部署形态:ssr: truessr: false

预渲染按 ssr 配置值有两种组合方式:

  • ssr: true(默认值):运行时 SSR 服务器之上做增量预渲染,加速部分页面的响应;
  • ssr: false完全去掉运行时服务器,产物部署到静态文件服务器(即静态站点生成)。

配合运行时服务器(ssr: true

ssr 默认即 true。此时你仍会部署一台运行时 SSR 服务器,但选择性地把某些路径预渲染成静态文件,避免这些请求打到服务器上:

import type { Config } from "@react-router/dev/config";

export default {
  // Can be omitted - defaults to true
  ssr: true,
  prerender: ["/", "/blog", "/blog/popular-post"],
} satisfies Config;

数据加载:与 SSR 完全同款 loader,无额外 API

预渲染没有引入任何额外的应用层 API。被预渲染的路径,数据加载仍使用与服务器渲染完全相同的路由 loader

export async function loader({ request, params }) {
  let post = await getPost(params.slug);
  return post;
}

export function Post({ loaderData }) {
  return <div>{loaderData.title}</div>;
}

差别仅在于请求来源:运行时是由真实用户请求触发,而构建时是由构建器创建一个 new Request(),像服务器一样把请求打穿整个应用(经过路由匹配 → loaderentry.server.tsx 渲染)。由于每次预渲染走的就是完整请求管线,所有 loader 错误、redirect 等行为都会在构建期真实暴露出来。

那么,对于没有被预渲染的路径呢?只要 ssr: true,这些路径在运行时仍由服务器照常 SSR,两者互不干扰——这正是“部分静态化”模式的意义所在。

静态文件输出与产物布局

渲染结果写入 build/client 目录。每个路径会生成两个文件:

  • [url].html:供浏览器首次文档请求使用的 HTML 文件;
  • [url].data:供浏览器**客户端导航(SPA 内跳转)**请求使用的数据文件,用于复用已预渲染的 loader 数据、避免二次请求。

构建输出会逐条打印预渲染产物的生成日志:

> react-router build
vite v5.2.11 building for production...
...
vite v5.2.11 building SSR bundle for production...
...
Prerender: Generated build/client/index.html
Prerender: Generated build/client/blog.data
Prerender: Generated build/client/blog/index.html
Prerender: Generated build/client/blog/my-first-post.data
Prerender: Generated build/client/blog/my-first-post/index.html
...

这套“两份文件”的写入逻辑与请求调度在 plugin.ts 中定义:对于含 loader 的路径,先通过 createDataRequest 发起数据请求,postProcess 在收到 200/202 响应后把内容写入 [path].data,随后自动追加一个 HTML 请求,把已渲染好的数据嵌入 HTML 响应后一并写出(见 plugin.ts);若响应非 200/202 则构建直接报错。HTML 文件的默认落盘规则是 [url]/index.html,即 /index.html/blogblog/index.html(见 plugin.ts)。此外,若路径在预渲染期间返回 301/302/303/307/308 重定向,构建器还会生成一个带 meta refresh 的静态重定向 HTML 页,便于在无法配置服务器级重定向的静态托管环境中兜底(见 plugin.ts)。

有一点需要强调:开发模式下预渲染并不会把结果保存到产物目录,只有执行 react-router build 时才会真实产出文件。开发期看到的仍是常规请求渲染。

纯静态部署(ssr: false

如果连运行时 SSR 服务器都不想要,直接禁用运行时渲染并把预渲染产物托管在静态文件服务器上:

import type { Config } from "@react-router/dev/config";

export default {
  ssr: false, // disable runtime server rendering
  prerender: true, // pre-render all static routes
} satisfies Config;

关于 ssr: falseprerender 的关系,原文档给出了三种分层结论:

  1. ssr: false 但完全不配置 prerender,即 SPA Mode。React Router 只渲染单个 HTML 文件(仅渲染 root 路由),它能对任意应用路径水合——因为根路由之外到底加载哪些子路由,要到浏览器端依据 URL 在水合时决定。正因如此,SPA Mode 下只允许在根路由使用 loader,其余路由不得使用(构建期无法预知要加载哪些路由)。详见 SPA 模式 文档。

  2. ssr: false 且配置了 prerender(覆盖部分或全部路径):这些被预渲染路径上的匹配到的所有路由都可以有 loader——因为构建期会把该路径的整条匹配链全部预渲染出来,而不仅是根路由。

  3. 无论是否预渲染,只要 ssr: false任何路由都不得包含 actionheaders 导出:因为没有运行时服务器来执行它们。

带 SPA Fallback 的部分预渲染

如果你既想要 ssr: false,又不想预渲染全部路由(部分页面要预渲染的性能/SEO 收益,其余页面用 SPA 即可),只需把 prerender 限定到你关心的路径。此时 React Router 会额外输出一个 "SPA Fallback" HTML 文件,采用与 SPA 模式 相同的思路水合其余任意路径。

该回退文件会写到以下两个位置之一:

  • build/client/index.html —— 当 / 路径未被预渲染时;
  • build/client/__spa-fallback.html —— 当 / 路径被预渲染时。
import type { Config } from "@react-router/dev/config";

export default {
  ssr: false,

  // SPA fallback will be written to build/client/index.html
  prerender: ["/about-us"],

  // SPA fallback will be written to build/client/__spa-fallback.html
  prerender: ["/", "/about-us"],
} satisfies Config;

SPA Fallback 的产出在源码中有明确校验:若返回的 HTML 中不包含 window.__reactRouterContextwindow.__reactRouterRouteModules 两个水合标记,构建会报错并提示“是否在根路由漏掉了 <Scripts/>”(见 plugin.ts);其默认落盘路径固定在 __spa-fallback.html(见 plugin.ts),随后在 finalize 阶段按 / 是否被预渲染决定是否改名/移动到 index.html

接下来需要在部署服务器上配置回退规则:把所有本应 404 的路径都指向这个回退文件,让客户端接管并水合对应页面。部分托管商默认支持,另一些则需显式配置,例如基于 _redirects 文件:

# If you did not pre-render the `/` route
/*    /index.html   200

# If you pre-rendered the `/` route
/*    /__spa-fallback.html   200

如果应用在合法路由上出现 404,极大概率就是托管商缺少这条回退规则。同理,用 sirv-cli 一类的静态服务器工具托管时:

# If you did not pre-render the `/` route
sirv-cli build/client --single index.html

# If you pre-rendered the `/` route
sirv-cli build/client --single __spa-fallback.html

Invalid Exports:构建期拦截配置错误

当使用 ssr: false 预渲染时,React Router 会在 构建期 直接报错,帮你拦住几类极易忽略的错误组合。规则汇总如下(其实现为 plugin.tsvalidateSsrFalsePrerenderExports,它先根据 prerender 路径反查哪些路由会被预渲染,再逐条校验导出):

  • headers / action 函数在所有路由上被禁止——没有运行时服务器可以执行它们;
  • 使用 ssr: false 且不配置 prerender(SPA Mode)时,只允许根路由loader
  • 使用 ssr: false 且配置了 prerender 时,任何被 prerender 路径匹配到的路由都允许有 loader
  • 如果一个使用了 loader 的路由带有子路由,必须保证父级 loaderData 在运行时能被正确解析,具体二选一:
    • 预渲染其全部子路由,使父级 loader 能在构建期为每条子路径调用一次并渲染进对应的 .data 文件;
    • 在父路由使用 clientLoader,使其能在运行时为非预渲染的子路径执行。

实操小结与排查路径

按“部署形态”快速选定配置组合:

目标 配置 产物特征
SSR 服务器 + 部分页面静态化 ssr: true(默认)+ prerender: [paths] 每路径 [url].html + [url].data,未预渲染路径运行时照常 SSR
纯静态站点(全量 SSG) ssr: false + prerender: true 所有静态路径产物;动态路径需用数组/函数显式枚举
纯静态 + 部分页面 SPA ssr: false + prerender: [部分路径] 额外产出 SPA Fallback(index.html__spa-fallback.html),需配置兜底转发
纯 SPA ssr: false、不写 prerender 单个可水合任意路径的 index.html,见 SPA 模式

若想继续深入:

最后,把这套能力放入正确的心智模型:预渲染不是另一套路由 API,而是 “把运行时请求提前到构建期执行一次,并把结果静态落盘”。对 loader 复用、数据协议、导出约束的上述理解,将直接决定你的静态化方案能否一次构建即正确上线。

登录后查看全文
热门项目推荐
相关项目推荐