使用 Preact 实现服务端渲染
如果你曾用 React 和 Vite 构建过应用,那你已经理解了客户端渲染的原理。浏览器下载 JavaScript bundle,执行它,然后构建 DOM。这种方式可行,但网络较慢的用户在 bundle 加载期间只能盯着空白屏幕。使用 Preact 进行服务端渲染则通过从服务器发送可直接显示的 HTML 来解决这一问题。Preact 极小的运行时体积让这种权衡对性能敏感的应用尤其有吸引力。
本文将介绍 Preact SSR 的工作原理、涉及的工具链,以及 hydration(注水)如何将服务端渲染的 HTML 与可交互的应用连接起来。
核心要点
- Preact SSR 在服务器端将组件树渲染为 HTML 字符串,让用户在任何 JavaScript 执行之前就能看到内容。
preact-render-to-string包提供了同步、异步和流式渲染 API,以适应不同的应用需求。- Hydration 通过
hydrate而非render为服务端渲染的 HTML 附加交互能力,从而保留已有的 DOM。 - 基于 Vite 的工作流配合
@preact/preset-vite是新建支持 SSR 的 Preact 项目的现代默认选择。 - 保持服务端与客户端输出一致以避免 hydration 不匹配——服务端渲染阶段不要使用时间戳、随机 ID 或访问
window。
什么是 Preact SSR,为什么它很重要?
借助 SSR,你的组件树会在服务器端被渲染为 HTML 字符串,然后浏览器才接收到任何内容。用户可以立即看到内容。JavaScript 在后台加载,并在加载完成后附加交互能力。
实际收益是真实存在的:
- 更快的感知加载速度——可见内容随首次 HTTP 响应一起到达。
- 更好的 SEO——爬虫无需执行 JavaScript 即可读取完整的 HTML。
- 更强的健壮性——即使客户端 JS 加载失败或缓慢,页面仍然可读。
Preact 尤其适合 SSR,因为其小巧的运行时带来的开销极低。
核心工具:preact-render-to-string
Preact SSR 依赖于 preact-render-to-string,这是一个负责服务端渲染的独立包。
npm install preact-render-to-string
同步渲染
对于没有异步依赖的组件,renderToString 会一次性将你的组件树转换为 HTML:
import { renderToString } from 'preact-render-to-string';
const App = () => <h1>Hello from the server</h1>;
const html = renderToString(<App />);
// → <h1>Hello from the server</h1>
异步渲染
当组件需要获取数据或使用带懒加载分块的 Suspense 时,应使用 renderToStringAsync。它会等待 Promise 和异步渲染工作完成后再返回最终的 HTML 字符串。
import { renderToStringAsync } from 'preact-render-to-string';
const html = await renderToStringAsync(<App />);
流式渲染
对于较大的页面,流式 API 允许你在每个部分渲染完成时将 HTML 分块发送到浏览器。renderToPipeableStream 面向 Node.js 流,而 renderToReadableStream 面向使用 Web Streams API 的环境,包括 Cloudflare Workers、Deno 和 Bun 等平台。流式渲染可以改善首字节时间(TTFB),而不必等待整个渲染完成。
Preact Hydration:连接 HTML 与交互能力
发送静态 HTML 只完成了一半工作。要让页面具有交互能力,Preact 需要对已存在的 DOM 进行 hydrate——附加事件监听器和状态,而不是从头重新渲染。
import { hydrate } from 'preact';
import { App } from './app.js';
hydrate(<App />, document.getElementById('root'));
当 DOM 已由服务器生成时,使用 hydrate 而非 render。使用 render 会丢弃服务端的 HTML 并重新构建,这就违背了 SSR 的初衷。
Hydration 不匹配发生在服务端渲染的 HTML 与客户端渲染结果不一致时。常见原因包括在服务端渲染时输出时间戳、随机 ID 或读取 window。保持服务端和客户端组件逻辑一致即可避免。
Discover how at OpenReplay.com.
Preact Vite SSR:现代化配置
对于新项目,基于 Vite 的工作流是务实的默认选择。Vite 的 SSR 指南 详细介绍了双构建模式:一份针对服务端入口,另一份针对客户端 bundle。Preact 通过 @preact/preset-vite 实现无缝集成,该插件处理 JSX、别名以及 Preact 特定的配置。
对于希望有更结构化起点的团队,preact-iso 提供了专为 Preact Vite 项目设计的轻量级路由和预渲染工具。
需要注意的事项
Preact SSR 在概念上与 React SSR 相通,但在具体实现细节上并不完全相同。一些 React 特有的 SSR API 没有直接对应物,而且 Preact 的生态系统相对较小——为了体积和性能的提升,这是一个合理的取舍。
可靠的工作模式是:先获取数据,再以 props 传入组件,在服务器端渲染为字符串,然后在客户端进行 hydration。从这里开始,等有了充分理由后,再叠加流式渲染或更高级的渲染模式。
结语
Preact SSR 提供了一条轻量级、注重性能的服务端渲染路径,无需承担更大框架的开销。通过组合使用 preact-render-to-string 进行服务端渲染、hydrate 实现客户端激活,以及 Vite 构建管道,你能得到一套发布迅速且易于理解的方案。从基础的渲染加 hydration 流程开始,保持服务端和客户端输出一致,只有当应用规模确实需要时才引入流式渲染。
常见问题
当 bundle 体积、冷启动性能或边缘部署比访问 React 更广阔的生态系统更重要时,选择 Preact SSR。Preact 小巧的运行时非常适合内容密集型站点、营销页面和基于 Workers 的部署。如果你依赖 React 特定的库,或需要 Preact 未实现的 API(如 React Server Components),则继续使用 React SSR。
确保服务端和客户端在相同 props 下渲染出相同的输出。避免在初次渲染时使用非确定性值,例如 Date.now、Math.random 或仅在浏览器端可用的全局对象如 window 和 localStorage。如果你需要展示仅客户端可用的内容,可在服务端渲染一个稳定的占位符,并在 hydration 完成后通过 effect 更新它。
许多 React 库可以通过 preact/compat 别名工作,它会将 React 的导入映射到 Preact 的等价实现。然而,依赖 React 特定 SSR 特性(如 React Server Components 或渲染内部机制)的库可能无法正常工作。在投入使用前,请在你的 SSR 流程中测试每个依赖。
可能不值得。流式渲染的价值在页面较大、数据密集,或包含解析速度不同的多个部分时才能显现。对于典型的小型站点,renderToString 或 renderToStringAsync 已经能足够快地产出结果,引入流式渲染只会增加复杂度而无可衡量的收益。建议先使用更简单的同步或异步 API,只有当 TTFB 真正成为瓶颈时再采用流式渲染。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.