异步渲染与 SSR “模式”
Async Rendering and SSR “Modes”
服务器渲染一个仅使用同步数据的页面非常简单:你只需遍历组件树,将每个元素渲染为 HTML 字符串。但这是一个很大的前提:它没有回答我们应该如何处理包含异步数据的页面,即在客户端会在 <Suspense/> 节点下渲染的那些内容。
Server-rendering a page that uses only synchronous data is pretty simple: You just walk down the component tree, rendering each element to an HTML string. But this is a pretty big caveat: it doesn’t answer the question of what we should do with pages that includes asynchronous data, i.e., the sort of stuff that would be rendered under a <Suspense/> node on the client.
当页面加载需要渲染的异步数据时,我们该怎么办?我们应该等待所有异步数据加载完毕,然后一次性渲染所有内容吗?(我们称之为“异步”渲染)我们应该走向完全相反的方向,立即将现有的 HTML 发送给客户端,让客户端加载资源并填补空缺吗?(我们称之为“同步”渲染)或者是否存在某种折中方案,能同时超越这两者?(提示:确实存在。)
When a page loads async data that it needs to render, what should we do? Should we wait for all the async data to load, and then render everything at once? (Let’s call this “async” rendering) Should we go all the way in the opposite direction, just sending the HTML we have immediately down to the client and letting the client load the resources and fill them in? (Let’s call this “synchronous” rendering) Or is there some middle-ground solution that somehow beats them both? (Hint: There is.)
如果你曾经在线听过流媒体音乐或看过去视频,我确信你意识到 HTTP 支持流式传输(streaming),允许单个连接陆续发送数据块,而无需等待全部内容加载完毕。你可能没有意识到浏览器也非常擅长渲染部分 HTML 页面。综上所述,这意味着你实际上可以通过 流式传输 HTML (streaming HTML) 来增强用户体验:这是 Leptos 开箱即用的功能,完全不需要任何配置。实际上,流式传输 HTML 有多种方式:你可以按顺序流式传输组成页面的 HTML 块,就像视频帧一样;或者你可以……嗯,不按顺序流式传输。
If you’ve ever listened to streaming music or watched a video online, I’m sure you realize that HTTP supports streaming, allowing a single connection to send chunks of data one after another without waiting for the full content to load. You may not realize that browsers are also really good at rendering partial HTML pages. Taken together, this means that you can actually enhance your users’ experience by streaming HTML: and this is something that Leptos supports out of the box, with no configuration at all. And there’s actually more than one way to stream HTML: you can stream the chunks of HTML that make up your page in order, like frames of a video, or you can stream them... well, out of order.
让我详细说明一下我的意思。
Let me say a little more about what I mean.
Leptos 支持包含异步数据的 HTML 渲染的所有主要方式:
Leptos supports all the major ways of rendering HTML that includes asynchronous data:
-
乱序流式传输(以及一种部分阻塞变体)
-
Out-of-Order Streaming (and a partially-blocked variant)
同步渲染
Synchronous Rendering
- 同步 (Synchronous):提供一个 HTML 外壳,其中包含任何
<Suspense/>的fallback(回退内容)。在客户端使用create_local_resource加载数据,一旦资源加载完毕,就替换fallback。
-
优点:应用外壳出现得非常快:首字节时间 (TTFB) 极佳。
-
Pros: App shell appears very quickly: great TTFB (time to first byte).
-
缺点
- 资源加载相对较慢;你甚至在发出请求之前需要等待 JS + WASM 加载。
- 无法在
<title>或其他<meta>标签中包含来自异步资源的数据,这会损害 SEO 和社交媒体链接预览等功能。
-
Cons
- Resources load relatively slowly; you need to wait for JS + WASM to load before even making a request.
- No ability to include data from async resources in the
<title>or other<meta>tags, hurting SEO and things like social media link previews.
从性能角度来看,如果你正在使用服务器端渲染,同步模式几乎从来不是你真正想要的。这是因为它错过了一个重要的优化。如果你在服务器渲染期间加载异步资源,你实际上可以在服务器上就开始加载数据。服务器渲染可以在客户端首次发起请求时就开始加载资源,而不是等待客户端接收 HTML 响应,然后加载其 JS + WASM,然后 意识到它需要资源并开始加载它们。从这个意义上说,在服务器渲染期间,异步资源就像一个在服务器上开始加载并在客户端上解析(resolve)的 Future。只要资源实际上是可序列化的,这总会带来更快的总加载时间。
If you’re using server-side rendering, the synchronous mode is almost never what you actually want, from a performance perspective. This is because it misses out on an important optimization. If you’re loading async resources during server rendering, you can actually begin loading the data on the server. Rather than waiting for the client to receive the HTML response, then loading its JS + WASM, then realize it needs the resources and begin loading them, server rendering can actually begin loading the resources when the client first makes the request. In this sense, during server rendering an async resource is like a Future that begins loading on the server and resolves on the client. As long as the resources are actually serializable, this will always lead to a faster total load time.
这就是为什么
Resource需要其数据是可序列化的,以及为什么你应该对任何不可序列化且因此只能在浏览器本身中加载的异步数据使用LocalResource。当你本可以创建可序列化资源时却创建本地资源,这总是一种去优化(deoptimization)。
This is why a
Resourceneeds its data to be serializable, and why you should useLocalResourcefor any async data that is not serializable and should therefore only be loaded in the browser itself. Creating a local resource when you could create a serializable resource is always a deoptimization.
异步渲染
Async Rendering
- 异步 (
async):在服务器上加载所有资源。等到所有数据加载完毕,然后一次性渲染 HTML。
-
优点:对元标签的处理更好(因为你在渲染
<head>之前就已经知道了异步数据)。比 同步 加载完成得更快,因为异步资源在服务器上就开始加载。 -
Pros: Better handling for meta tags (because you know async data even before you render the
<head>). Faster complete load than synchronous because async resources begin loading on server. -
缺点:加载时间/TTFB 较慢:你需要等待所有异步资源加载完毕才能在客户端显示任何内容。在所有内容加载完毕之前,页面完全是空白的。
-
Cons: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client. The page is totally blank until everything is loaded.
顺序流式传输
In-Order Streaming
- 顺序流式传输 (In-order streaming):遍历组件树,渲染 HTML 直到遇到
<Suspense/>。将到目前为止获得的所有 HTML 作为一个块发送到流中,等待该<Suspense/>下访问的所有资源加载完毕,然后将其渲染为 HTML 并继续遍历,直到遇到另一个<Suspense/>或页面结束。
-
优点:与空白屏幕相比,在数据准备好之前至少能显示 某些内容。
-
Pros: Rather than a blank screen, shows at least something before the data are ready.
-
缺点
- 加载外壳的速度比同步渲染(或乱序流式传输)慢,因为它需要在每个
<Suspense/>处暂停。 - 无法显示
<Suspense/>的回退(fallback)状态。 - 在整个页面加载完毕之前无法开始注水(hydration),因此在悬停块加载完毕之前,页面的早期部分将不可交互。
- 加载外壳的速度比同步渲染(或乱序流式传输)慢,因为它需要在每个
-
Cons
- Loads the shell more slowly than synchronous rendering (or out-of-order streaming) because it needs to pause at every
<Suspense/>. - Unable to show fallback states for
<Suspense/>. - Can’t begin hydration until the entire page has loaded, so earlier pieces of the page will not be interactive until the suspended chunks have loaded.
- Loads the shell more slowly than synchronous rendering (or out-of-order streaming) because it needs to pause at every
乱序流式传输
Out-of-Order Streaming
- 乱序流式传输 (Out-of-order streaming):类似于同步渲染,提供一个包含任何
<Suspense/>的fallback的 HTML 外壳。但在 服务器 上加载数据,并在数据解析(resolve)时将其流式传输到客户端,同时流式传输<Suspense/>节点的 HTML,这些 HTML 会被交换以替换 fallback。
-
优点:结合了 同步 和 异步 (
async) 的优点。- 初始响应/TTFB 快,因为它立即发送整个同步外壳。
- 总时间快,因为资源在服务器上就开始加载。
- 能够显示回退加载状态并动态替换它,而不是为未加载的数据显示空白部分。
-
Pros: Combines the best of synchronous and
async.- Fast initial response/TTFB because it immediately sends the whole synchronous shell
- Fast total time because resources begin loading on the server.
- Able to show the fallback loading state and dynamically replace it, instead of showing blank sections for un-loaded data.
-
缺点:需要启用 JavaScript,悬停片段才能以正确的顺序出现。(这段少量的 JS 随包含已渲染
<Suspense/>片段的<template>标签一起通过<script>标签流式传输,因此不需要加载任何额外的 JS 文件。) -
Cons: Requires JavaScript to be enabled for suspended fragments to appear in correct order. (This small chunk of JS streamed down in a
<script>tag alongside the<template>tag that contains the rendered<Suspense/>fragment, so it does not need to load any additional JS files.)
- 部分阻塞流式传输 (Partially-blocked streaming):当页面上有多个独立的
<Suspense/>组件时,“部分阻塞”流式传输非常有用。它通过在路由上设置ssr=SsrMode::PartiallyBlocked触发,并取决于视图中的阻塞资源。如果其中一个<Suspense/>组件读取一个或多个“阻塞资源”(见下文),则不会发送 fallback;相反,服务器将等待该<Suspense/>解析完毕,然后在服务器上用解析后的片段替换 fallback,这意味着它包含在初始 HTML 响应中,即使 JavaScript 被禁用或不支持也能显示。其他<Suspense/>则以乱序方式流入,类似于默认的SsrMode::OutOfOrder。
当页面上有多个 <Suspense/> 且其中一个比另一个更重要时,这非常有用:想象一下博客文章和评论,或者产品信息和评论。如果只有一个 <Suspense/>,或者每个 <Suspense/> 都读取阻塞资源,那么它就 不 有用。在这些情况下,它是 async 渲染的一种较慢的形式。
This is useful when you have multiple <Suspense/> on the page, and one is more important than the other: think of a blog post and comments, or product information and reviews. It is not useful if there’s only one <Suspense/>, or if every <Suspense/> reads from blocking resources. In those cases it is a slower form of async rendering.
-
优点:在用户设备上禁用或不支持 JavaScript 的情况下也能工作。
-
Pros: Works if JavaScript is disabled or not supported on the user’s device.
-
缺点
- 初始响应时间比乱序流式传输慢。
- 由于服务器上的额外工作,整体响应略慢。
- 不显示回退状态。
-
Cons
- Slower initial response time than out-of-order.
- Marginally slower overall response due to additional work on the server.
- No fallback state shown.
使用 SSR 模式
Using SSR Modes
因为乱序流式传输提供了性能特征的最佳结合,Leptos 默认使用乱序流式传输。但选择加入这些不同的模式非常简单。你可以通过在你的一个或多个 <Route/> 组件上添加 ssr 属性来实现,就像 ssr_modes 示例 中那样。
Because it offers the best blend of performance characteristics, Leptos defaults to out-of-order streaming. But it’s really simple to opt into these different modes. You do it by adding an ssr property onto one or more of your <Route/> components, like in the ssr_modes example.
<Routes fallback=|| "Not found.">
// 我们将使用乱序流式传输和 <Suspense/> 加载主页
// We’ll load the home page with out-of-order streaming and <Suspense/>
<Route path=path!("") view=HomePage/>
// 我们将使用异步渲染加载文章,以便它们可以在加载数据 *后* 设置标题和元数据
// We'll load the posts with async rendering, so they can set
// the title and metadata *after* loading the data
<Route
path=path!("/post/:id")
view=BlogPost
ssr=SsrMode::Async
/>
</Routes>
对于包含多个嵌套路由的路径,将使用限制最严格的模式:即,即使只有一个嵌套路由要求 async 渲染,整个初始请求都将以 async 方式渲染。async 是要求最严格的,其次是顺序流式(in-order),然后是乱序流式(out-of-order)。(如果你思考几分钟,这可能就讲得通了。)
For a path that includes multiple nested routes, the most restrictive mode will be used: i.e., if even a single nested route asks for async rendering, the whole initial request will be rendered async. async is the most restricted requirement, followed by in-order, and then out-of-order. (This probably makes sense if you think about it for a few minutes.)
阻塞资源
Blocking Resources
阻塞资源可以使用 Resource::new_blocking 创建。阻塞资源仍然像 Rust 中的任何其他 async/.await 一样异步加载。它不会阻塞服务器线程或类似的东西。相反,在 <Suspense/> 下读取阻塞资源会阻塞 HTML 流 返回任何内容(包括其初始同步外壳),直到该 <Suspense/> 解析完毕。
Blocking resources can be created with Resource::new_blocking. A blocking resource still loads asynchronously like any other async/.await in Rust. It doesn’t block a server thread, or anything like that. Instead, reading from a blocking resource under a <Suspense/> blocks the HTML stream from returning anything, including its initial synchronous shell, until that <Suspense/> has resolved.
从性能角度来看,这并不理想。在资源准备就绪之前,页面的同步外壳都不会加载。然而,什么都不渲染意味着你可以在真实的 HTML 中设置 <head> 中的 <title> 或 <meta> 标签等内容。这听起来很像 async 渲染,但有一个很大的区别:如果你有多个 <Suspense/> 部分,你可以阻塞其中 一个,但仍然渲染占位符,然后流式传输另一个。
From a performance perspective, this is not ideal. None of the synchronous shell for your page will load until that resource is ready. However, rendering nothing means that you can do things like set the <title> or <meta> tags in your <head> in actual HTML. This sounds a lot like async rendering, but there’s one big difference: if you have multiple <Suspense/> sections, you can block on one of them but still render a placeholder and then stream in the other.
例如,考虑一篇博客文章。为了 SEO 和社交分享,我肯定希望在初始 HTML 的 <head> 中包含我的博客文章标题和元数据。但我真的不在乎评论是否已经加载;我想尽可能懒惰地加载它们。
For example, think about a blog post. For SEO and for social sharing, I definitely want my blog post’s title and metadata in the initial HTML <head>. But I really don’t care whether comments have loaded yet or not; I’d like to load those as lazily as possible.
通过阻塞资源,我可以这样做:
With blocking resources, I can do something like this:
#[component]
pub fn BlogPost() -> impl IntoView {
let post_data = Resource::new_blocking(/* 加载博客文章 */);
let comments_data = Resource::new(/* 加载博客评论 */);
view! {
<Suspense fallback=|| ()>
{move || Suspend::new(async move {
let data = post_data.await;
view! {
<Title text=data.title/>
<Meta name="description" content=data.excerpt/>
<article>
/* 渲染文章内容 */
</article>
}
})}
</Suspense>
<Suspense fallback=|| "Loading comments...">
{move || Suspend::new(async move {
let comments = comments_data.await;
todo!()
})}
</Suspense>
}
}
第一个带有博客文章主体的 <Suspense/> 将阻塞我的 HTML 流,因为它读取了一个阻塞资源。在流发送之前,正在等待阻塞资源的元标签(Meta tags)和其他头部元素将被渲染。
The first <Suspense/>, with the body of the blog post, will block my HTML stream, because it reads from a blocking resource. Meta tags and other head elements awaiting the blocking resource will be rendered before the stream is sent.
结合以下使用 SsrMode::PartiallyBlocked 的路由定义,阻塞资源将在服务器端被完全渲染,使得禁用了 WebAssembly 或 JavaScript 的用户也可以访问。
Combined with the following route definition, which uses SsrMode::PartiallyBlocked, the blocking resource will be fully rendered on the server side, making it accessible to users who disable WebAssembly or JavaScript.
<Routes fallback=|| "Not found.">
// 我们将使用乱序流式传输和 <Suspense/> 加载主页
// We’ll load the home page with out-of-order streaming and <Suspense/>
<Route path=path!("") view=HomePage/>
// 我们将使用异步渲染加载文章,以便它们可以在加载数据 *后* 设置标题和元数据
// We'll load the posts with async rendering, so they can set
// the title and metadata *after* loading the data
<Route
path=path!("/post/:id")
view=BlogPost
ssr=SsrMode::PartiallyBlocked
/>
</Routes>
第二个带有评论的 <Suspense/> 将不会阻塞流。阻塞资源恰好赋予了我所需的权力和细粒度,以针对 SEO 和用户体验优化我的页面。
The second <Suspense/>, with the comments, will not block the stream. Blocking resources gave me exactly the power and granularity I needed to optimize my page for SEO and user experience.