React Router 预渲染(Pre-Rendering)深度指南:构建期渲染配置、并发控制与 SSR/SPA 双模式实战
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.ts 与 packages/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: true 与 ssr: 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(),像服务器一样把请求打穿整个应用(经过路由匹配 → loader → entry.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、/blog → blog/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: false 与 prerender 的关系,原文档给出了三种分层结论:
-
ssr: false但完全不配置prerender,即 SPA Mode。React Router 只渲染单个 HTML 文件(仅渲染root路由),它能对任意应用路径水合——因为根路由之外到底加载哪些子路由,要到浏览器端依据 URL 在水合时决定。正因如此,SPA Mode 下只允许在根路由使用loader,其余路由不得使用(构建期无法预知要加载哪些路由)。详见 SPA 模式 文档。 -
ssr: false且配置了prerender(覆盖部分或全部路径):这些被预渲染路径上的匹配到的所有路由都可以有loader——因为构建期会把该路径的整条匹配链全部预渲染出来,而不仅是根路由。 -
无论是否预渲染,只要
ssr: false,任何路由都不得包含action或headers导出:因为没有运行时服务器来执行它们。
带 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.__reactRouterContext 与 window.__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.ts 的 validateSsrFalsePrerenderExports,它先根据 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 模式 |
若想继续深入:
- 配置类型与校验:见 config/config.ts
- 路径推导(静态路径递归、参数路径剔除):见 plugin.ts
- 请求调度与文件产出(
.data、HTML、SPA Fallback、重定向兜底):见 plugin.ts - 底层 Vite 插件(并发、超时、重试、preview server 驱动):见 plugins/prerender.ts
- 端到端行为验证用例:见 integration/vite-prerender-test.ts
最后,把这套能力放入正确的心智模型:预渲染不是另一套路由 API,而是 “把运行时请求提前到构建期执行一次,并把结果静态落盘”。对 loader 复用、数据协议、导出约束的上述理解,将直接决定你的静态化方案能否一次构建即正确上线。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00