renderToPipeableStream 将 React 树渲染为可管道化的 Node.js 流
const { pipe, abort } = renderToPipeableStream(reactNode, options?)参考
🌐 Reference
renderToPipeableStream(reactNode, options?)
调用 renderToPipeableStream 将你的 React 树渲染为 HTML 到 Node.js 流 中
🌐 Call renderToPipeableStream to render your React tree as HTML into a Node.js Stream.
import { renderToPipeableStream } from 'react-dom/server';
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});在客户端,调用 hydrateRoot 以使服务器生成的 HTML 具有交互性。
🌐 On the client, call hydrateRoot to make the server-generated HTML interactive.
参数
🌐 Parameters
reactNode:你想渲染为 HTML 的 React 节点。例如,像<App />这样的 JSX 元素。它预计代表整个文档,因此App组件应渲染<html>标签。- 可选
options:一个带有流选项的对象。- 可选
bootstrapScriptContent:如果指定,此字符串将被放置在内联<script>标签中。 - 可选
bootstrapScripts:一个字符串 URL 数组,用于在页面上发出<script>标签。使用它来包含调用hydrateRoot. 的<script>。如果你完全不想在客户端运行 React,则可以省略它。 - 可选
bootstrapModules:像bootstrapScripts,但发出<script type="module">。 - 可选
identifierPrefix:React 用于useId. 生成的 ID 的字符串前缀。在同一页面使用多个根节点时,有助于避免冲突。必须与传递给hydrateRoot. 的前缀相同。 - 可选
namespaceURI:一个包含流的根命名空间 URI的字符串。默认是普通 HTML。传递'http://www.w3.org/2000/svg'表示 SVG,传递'http://www.w3.org/1998/Math/MathML'表示 MathML。 - 可选
nonce:一个nonce字符串,用于允许script-src内容安全策略`的脚本。 - 可选
onAllReady:一个回调,当包括shell和所有额外内容在内的所有渲染完成时触发。你可以使用它来代替onShellReady用于爬虫和静态生成。如果你从这里开始流式输出,你将无法获得任何渐进式加载。流将包含最终的HTML。 - 可选
onError:每当发生服务器错误时触发的回调,无论是可恢复的还是不可恢复的。默认情况下,这只会调用console.error。如果你重写它以记录崩溃报告,请确保仍然调用console.error。你也可以在发出 shell 之前使用它来调整状态码。 - 可选
onShellReady:一个回调函数,在 初始 shell 渲染完毕后立即触发。你可以在这里 设置状态码 并调用pipe开始流式传输。React 会在 shell 之后 流式传输额外内容,以及使用内联<script>标签将 HTML 加载回退替换为内容。 - 可选
onShellError:如果在渲染初始 shell 时发生错误,会触发的回调。它接收错误作为参数。流中还没有输出任何字节,且onShellReady和onAllReady都不会被调用,因此你可以输出一个备用的 HTML shell。 - 可选
progressiveChunkSize:一个块中的字节数。了解有关默认启发式方法的更多信息。
- 可选
返回
🌐 Returns
renderToPipeableStream 返回一个包含两个方法的对象:
pipe将 HTML 输出到提供的 Writable Node.js 流。 如果你想启用流式传输,请在onShellReady中调用pipe,或者在onAllReady中为爬虫和静态生成调用它。abort允许你中止服务器渲染并在客户端渲染其余部分。
用法
🌐 Usage
将 React 树作为 HTML 渲染到 Node.js 流
🌐 Rendering a React tree as HTML to a Node.js Stream
调用 renderToPipeableStream 将你的 React 树渲染为 HTML 到 Node.js 流:
🌐 Call renderToPipeableStream to render your React tree as HTML into a Node.js Stream:
import { renderToPipeableStream } from 'react-dom/server';
// The route handler syntax depends on your backend framework
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});除了 根组件之外,你还需要提供一个 引导 <script> 路径的列表。你的根组件应返回包括根 <html> 标签在内的整个文档。
例如,它可能看起来像这样:
🌐 For example, it might look like this:
export default function App() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/styles.css"></link>
<title>My app</title>
</head>
<body>
<Router />
</body>
</html>
);
}React 将把 doctype 和你的 bootstrap <script> 标签 注入到生成的 HTML 流中:
<!DOCTYPE html>
<html>
<!-- ... HTML from your components ... -->
</html>
<script src="/main.js" async=""></script>在客户端,你的引导脚本应该通过调用 hydrateRoot 来 hydrating 整个 document
🌐 On the client, your bootstrap script should hydrate the entire document with a call to hydrateRoot:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);这会将事件监听器附加到服务器生成的 HTML 并使其具有交互性。
🌐 This will attach event listeners to the server-generated HTML and make it interactive.
深入研究
🌐 Reading CSS and JS asset paths from the build output
最终的资源 URL(如 JavaScript 和 CSS 文件)通常在构建后会被哈希。例如,你可能不会得到 styles.css,而是 styles.123456.css。对静态资源文件名进行哈希可以保证同一资源的每次不同构建都拥有不同的文件名。这很有用,因为它可以让你安全地为静态资源启用长期缓存:具有特定名称的文件内容永远不会改变。
🌐 The final asset URLs (like JavaScript and CSS files) are often hashed after the build. For example, instead of styles.css you might end up with styles.123456.css. Hashing static asset filenames guarantees that every distinct build of the same asset will have a different filename. This is useful because it lets you safely enable long-term caching for static assets: a file with a certain name would never change content.
然而,如果你在构建之后才知道资源的 URL,就无法将它们放入源代码中。例如,像之前那样将 "/styles.css" 硬编码到 JSX 中是行不通的。为了将它们保留在源代码之外,你的根组件可以从作为 prop 传入的映射中读取真实的文件名:
🌐 However, if you don’t know the asset URLs until after the build, there’s no way for you to put them in the source code. For example, hardcoding "/styles.css" into JSX like earlier wouldn’t work. To keep them out of your source code, your root component can read the real filenames from a map passed as a prop:
export default function App({ assetMap }) {
return (
<html>
<head>
...
<link rel="stylesheet" href={assetMap['styles.css']}></link>
...
</head>
...
</html>
);
}在服务器上,渲染 <App assetMap={assetMap} /> 并传递你的 assetMap 以及资源 URL:
🌐 On the server, render <App assetMap={assetMap} /> and pass your assetMap with the asset URLs:
// You'd need to get this JSON from your build tooling, e.g. read it from the build output.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['main.js']],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});由于你的服务器现在正在渲染 <App assetMap={assetMap} />,你也需要在客户端使用 assetMap 进行渲染以避免水合错误。你可以像这样序列化并传递 assetMap 给客户端:
🌐 Since your server is now rendering <App assetMap={assetMap} />, you need to render it with assetMap on the client too to avoid hydration errors. You can serialize and pass assetMap to the client like this:
// You'd need to get this JSON from your build tooling.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {
// Careful: It's safe to stringify() this because this data isn't user-generated.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['main.js']],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});在上面的例子中,bootstrapScriptContent 选项添加了一个额外的内联 <script> 标签,该标签在客户端设置全局 window.assetMap 变量。这让客户端代码可以读取相同的 assetMap:
🌐 In the example above, the bootstrapScriptContent option adds an extra inline <script> tag that sets the global window.assetMap variable on the client. This lets the client code read the same assetMap:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);客户端和服务器端都使用相同的 assetMap 属性渲染 App,因此不会出现 hydration 错误。
🌐 Both client and server render App with the same assetMap prop, so there are no hydration errors.
在加载时流式传输更多内容
🌐 Streaming more content as it loads
流媒体允许用户在服务器上所有数据完全加载之前就开始查看内容。例如,考虑一个显示封面、带有朋友和照片侧边栏以及帖子列表的个人资料页面:
🌐 Streaming allows the user to start seeing the content even before all the data has loaded on the server. For example, consider a profile page that shows a cover, a sidebar with friends and photos, and a list of posts:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Posts />
</ProfileLayout>
);
}想象一下,为 <Posts /> 加载数据需要一些时间。理想情况下,你希望在不等待帖子加载的情况下向用户显示个人资料页面的其余内容。为此,将 Posts 封装在 <Suspense> 边界中:
🌐 Imagine that loading data for <Posts /> takes some time. Ideally, you’d want to show the rest of the profile page content to the user without waiting for the posts. To do this, wrap Posts in a <Suspense> boundary:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}这告诉 React 在 Posts 加载其数据之前开始流式传输 HTML。React 会首先发送加载回退(PostsGlimmer)的 HTML,然后,当 Posts 完成其数据加载时,React 会发送剩余的 HTML 以及一个内联的 <script> 标签,该标签将加载回退替换为该 HTML。从用户的角度来看,页面最初将显示 PostsGlimmer,随后被 Posts 替换。
🌐 This tells React to start streaming the HTML before Posts loads its data. React will send the HTML for the loading fallback (PostsGlimmer) first, and then, when Posts finishes loading its data, React will send the remaining HTML along with an inline <script> tag that replaces the loading fallback with that HTML. From the user’s perspective, the page will first appear with the PostsGlimmer, later replaced by the Posts.
你可以进一步嵌套 <Suspense> 边界来创建更细粒度的加载顺序:
🌐 You can further nest <Suspense> boundaries to create a more granular loading sequence:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}在这个例子中,React 可以更早地开始流式传输页面。只有 ProfileLayout 和 ProfileCover 必须先完成渲染,因为它们没有被封装在任何 <Suspense> 边界中。然而,如果 Sidebar、Friends 或 Photos 需要加载一些数据,React 将会先发送 BigSpinner 回退的 HTML。然后,随着更多数据的可用,更多内容将会继续显现,直到全部内容可见。
🌐 In this example, React can start streaming the page even earlier. Only ProfileLayout and ProfileCover must finish rendering first because they are not wrapped in any <Suspense> boundary. However, if Sidebar, Friends, or Photos need to load some data, React will send the HTML for the BigSpinner fallback instead. Then, as more data becomes available, more content will continue to be revealed until all of it becomes visible.
流式传输不需要等待 React 本身在浏览器中加载,也不需要等你的应用变得可交互。来自服务器的 HTML 内容会在任何 <script> 标签加载之前逐步显示出来。
🌐 Streaming does not need to wait for React itself to load in the browser, or for your app to become interactive. The HTML content from the server will get progressively revealed before any of the <script> tags load.
指定进入 shell 的内容
🌐 Specifying what goes into the shell
你的应用中任何 <Suspense> 边界之外的部分称为外壳:
🌐 The part of your app outside of any <Suspense> boundaries is called the shell:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}它决定了用户可能看到的最早加载状态:
🌐 It determines the earliest loading state that the user may see:
<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>如果你将整个应用封装在根部的 <Suspense> 边界中,shell 将只包含那个加载指示器。然而,这并不是一个愉快的用户体验,因为在屏幕上看到一个大加载指示器会让人感觉比多等一会儿看到真实布局更慢、更烦人。这就是为什么通常你会希望放置 <Suspense> 边界,使得 shell 感觉最小但完整——就像整个页面布局的骨架。
🌐 If you wrap the whole app into a <Suspense> boundary at the root, the shell will only contain that spinner. However, that’s not a pleasant user experience because seeing a big spinner on the screen can feel slower and more annoying than waiting a bit more and seeing the real layout. This is why usually you’ll want to place the <Suspense> boundaries so that the shell feels minimal but complete—like a skeleton of the entire page layout.
onShellReady 回调在整个 shell 渲染完成时触发。通常,你会在那时开始流式传输:
🌐 The onShellReady callback fires when the entire shell has been rendered. Usually, you’ll start streaming then:
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});到 onShellReady 触发时,嵌套 <Suspense> 边界中的组件可能仍在加载数据。
🌐 By the time onShellReady fires, components in nested <Suspense> boundaries might still be loading data.
服务器上的日志记录崩溃
🌐 Logging crashes on the server
默认情况下,服务器上的所有错误都会记录到控制台。你可以覆盖此行为来记录崩溃报告:
🌐 By default, all errors on the server are logged to console. You can override this behavior to log crash reports:
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});如果你提供自定义的 onError 实现,也别忘了像上面一样将错误记录到控制台。
🌐 If you provide a custom onError implementation, don’t forget to also log errors to the console like above.
从 shell 中的错误中恢复
🌐 Recovering from errors inside the shell
在这个例子中,壳包含 ProfileLayout、ProfileCover 和 PostsGlimmer:
🌐 In this example, the shell contains ProfileLayout, ProfileCover, and PostsGlimmer:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}如果在渲染这些组件时发生错误,React 将没有任何有意义的 HTML 可以发送给客户端。最后的手段是覆盖 onShellError,以发送不依赖于服务器渲染的备用 HTML:
🌐 If an error occurs while rendering those components, React won’t have any meaningful HTML to send to the client. Override onShellError to send a fallback HTML that doesn’t rely on server rendering as the last resort:
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});如果在生成 shell 时发生错误,onError 和 onShellError 都会触发。使用 onError 进行错误报告,使用 onShellError 发送备用 HTML 文档。你的备用 HTML 不必是错误页面。相反,你可以包含一个仅在客户端呈现你的应用的备用 shell。
🌐 If there is an error while generating the shell, both onError and onShellError will fire. Use onError for error reporting and use onShellError to send the fallback HTML document. Your fallback HTML does not have to be an error page. Instead, you may include an alternative shell that renders your app on the client only.
从 shell 外部的错误中恢复
🌐 Recovering from errors outside the shell
在这个示例中,<Posts /> 组件被封装在 <Suspense> 中,因此它不是 shell 的一部分:
🌐 In this example, the <Posts /> component is wrapped in <Suspense> so it is not a part of the shell:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}如果 Posts 组件或其内部的某个地方发生错误,React 将尝试从中恢复:
🌐 If an error happens in the Posts component or somewhere inside it, React will try to recover from it:
- 它将把最近的
<Suspense>边界 (PostsGlimmer) 的加载回退发出到 HTML 中。 - 它将“不再尝试”在服务器上渲染
Posts内容。 - 当 JavaScript 代码在客户端加载时,React 将在客户端重试渲染
Posts。
如果在客户端重试渲染 Posts 也 失败,React 将在客户端抛出错误。与渲染期间抛出的所有错误一样,最近的父级错误边界 决定如何向用户展示错误。实际上,这意味着用户将看到加载指示器,直到确认该错误无法恢复为止。
🌐 If retrying rendering Posts on the client also fails, React will throw the error on the client. As with all the errors thrown during rendering, the closest parent error boundary determines how to present the error to the user. In practice, this means that the user will see a loading indicator until it is certain that the error is not recoverable.
如果在客户端重试渲染 Posts 成功,来自服务器的加载备用内容将被客户端渲染输出替换。用户不会知道服务器出现了错误。然而,服务器 onError 回调和客户端 onRecoverableError 回调将会触发,以便你可以收到错误通知。
🌐 If retrying rendering Posts on the client succeeds, the loading fallback from the server will be replaced with the client rendering output. The user will not know that there was a server error. However, the server onError callback and the client onRecoverableError callbacks will fire so that you can get notified about the error.
设置状态码
🌐 Setting the status code
流式传输引入了一种权衡。你希望尽早开始流式传输页面,以便用户可以更快看到内容。然而,一旦开始流式传输,就无法再设置响应状态码。
🌐 Streaming introduces a tradeoff. You want to start streaming the page as early as possible so that the user can see the content sooner. However, once you start streaming, you can no longer set the response status code.
通过将你的应用划分为壳层(尤其是 <Suspense> 边界之上)和其余内容,你已经解决了这个问题的一部分。如果壳层出现错误,你会收到 onShellError 回调,这让你可以设置错误状态码。否则,你就知道应用可能在客户端恢复,因此你可以发送 “OK”。
🌐 By dividing your app into the shell (above all <Suspense> boundaries) and the rest of the content, you’ve already solved a part of this problem. If the shell errors, you’ll get the onShellError callback which lets you set the error status code. Otherwise, you know that the app may recover on the client, so you can send “OK”.
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = 200;
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});如果壳体外部的组件(即在 <Suspense> 边界内)抛出错误,React 不会停止渲染。这意味着 onError 回调会触发,但你仍将得到 onShellReady 而不是 onShellError。这是因为 React 会尝试在客户端从该错误中恢复,如上所述。
🌐 If a component outside the shell (i.e. inside a <Suspense> boundary) throws an error, React will not stop rendering. This means that the onError callback will fire, but you will still get onShellReady instead of onShellError. This is because React will try to recover from that error on the client, as described above.
但是,如果你愿意,你可以使用出现错误的事实来设置状态代码:
🌐 However, if you’d like, you can use the fact that something has errored to set the status code:
let didError = false;
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});这只会捕获在生成初始 shell 内容时发生的 shell 外部错误,因此它不是全面的。如果知道某些内容是否发生错误至关重要,你可以将其移入 shell 中。
🌐 This will only catch errors outside the shell that happened while generating the initial shell content, so it’s not exhaustive. If knowing whether an error occurred for some content is critical, you can move it up into the shell.
以不同的方式处理不同的错误
🌐 Handling different errors in different ways
你可以创建你自己的 Error 子类,并使用 instanceof 操作符来检查抛出了哪种错误。例如,你可以定义一个自定义的 NotFoundError 并从你的组件中抛出它。然后,你的 onError、onShellReady 和 onShellError 回调可以根据错误类型执行不同的操作:
🌐 You can create your own Error subclasses and use the instanceof operator to check which error is thrown. For example, you can define a custom NotFoundError and throw it from your component. Then your onError, onShellReady, and onShellError callbacks can do something different depending on the error type:
let didError = false;
let caughtError = null;
function getStatusCode() {
if (didError) {
if (caughtError instanceof NotFoundError) {
return 404;
} else {
return 500;
}
} else {
return 200;
}
}
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = getStatusCode();
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = getStatusCode();
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
didError = true;
caughtError = error;
console.error(error);
logServerCrashReport(error);
}
});请记住,一旦发出 shell 并开始流式传输,就无法更改状态代码。
🌐 Keep in mind that once you emit the shell and start streaming, you can’t change the status code.
等待为爬虫和静态生成加载所有内容
🌐 Waiting for all content to load for crawlers and static generation
流式提供了更好的用户体验,因为用户可以在内容可用时看到内容。
🌐 Streaming offers a better user experience because the user can see the content as it becomes available.
但是,当爬虫访问你的页面时,或者如果你在构建时生成页面,你可能希望首先加载所有内容,然后生成最终的 HTML 输出,而不是逐步显示它。
🌐 However, when a crawler visits your page, or if you’re generating the pages at the build time, you might want to let all of the content load first and then produce the final HTML output instead of revealing it progressively.
你可以使用 onAllReady 回调等待所有内容加载完成:
🌐 You can wait for all the content to load using the onAllReady callback:
let didError = false;
let isCrawler = // ... depends on your bot detection strategy ...
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
if (!isCrawler) {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
}
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onAllReady() {
if (isCrawler) {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
}
},
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});常规访客会获得逐步加载的内容流。爬虫则会在所有数据加载完成后接收最终的 HTML 输出。然而,这也意味着爬虫必须等待所有数据,其中一些数据可能加载较慢或出错。根据你的应用,你也可以选择向爬虫发送壳体。
🌐 A regular visitor will get a stream of progressively loaded content. A crawler will receive the final HTML output after all the data loads. However, this also means that the crawler will have to wait for all data, some of which might be slow to load or error. Depending on your app, you could choose to send the shell to the crawlers too.
中止服务器渲染
🌐 Aborting server rendering
你可以在超时后强制服务器渲染“放弃”:
🌐 You can force the server rendering to “give up” after a timeout:
const { pipe, abort } = renderToPipeableStream(<App />, {
// ...
});
setTimeout(() => {
abort();
}, 10000);React 会将剩余的加载回退作为 HTML 刷新,并将尝试在客户端渲染其余部分。
🌐 React will flush the remaining loading fallbacks as HTML, and will attempt to render the rest on the client.