渲染模式
我们可以通过服务器端渲染来减少交互时间。当需要生成一个导航文件,我们可以将其拆分为更小的块!Node流允许我们将数据流式传输到响应对象,这意味着我们可以持续向客户端发送数据。客户端一旦接收到数据块,就可以开始渲染内容。
此处为语雀视频卡片,点击链接查看:ssr-1.webm
React的内置renderToNodeStream
方法使我们能够将代码以小块的形式发送。由于客户端可以在接收数据的同时开始绘制UI,我们可以非常快速的加载首页。
此处为语雀视频卡片,点击链接查看:ssr-2.webm
假设我们有一个应用程序,在App
组件中向用户展示数千条关于猫的信息!
jsx
import React from "react";
import path from "path";
import express from "express";
import { renderToNodeStream } from "react-dom/server";
import App from "./src/App";
const app = express();
app.get("/favicon.ico", (req, res) => res.end());
app.use("/client.js", (req, res) => res.redirect("/build/client.js"));
const DELAY = 500;
app.use((req, res, next) => {
setTimeout(() => {
next();
}, DELAY);
});
const BEFORE = `
<!DOCTYPE html>
<html>
<head>
<title>Cat Facts</title>
<link rel="stylesheet" href="/style.css">
<script type="module" defer src="/build/client.js"></script>
</head>
<body>
<h1>Stream Rendered Cat Facts!</h1>
<div id="approot">
`.replace(/\n\s*/g, "");
app.get("/", async (request, response) => {
try {
const stream = renderToNodeStream(<App />);
const start = Date.now();
stream.on("data", function handleData() {
console.log("Render Start: ", Date.now() - start);
stream.off("data", handleData);
response.useChunkedEncodingByDefault = true;
response.writeHead(200, {
"content-type": "text/html",
"content-transfer-encoding": "chunked",
"x-content-type-options": "nosniff"
});
response.write(BEFORE);
response.flushHeaders();
});
await new Promise((resolve, reject) => {
stream.on("error", err => {
stream.unpipe(response);
reject(err);
});
stream.on("end", () => {
console.log("Render End: ", Date.now() - start);
response.write("</div></body></html>");
response.end();
resolve();
});
stream.pipe(
response,
{ end: false }
);
});
} catch (err) {
response.writeHead(500, {
"content-type": "text/pain"
});
response.end(String((err && err.stack) || err));
return;
}
});
app.use(express.static(path.resolve(__dirname, "src")));
app.use("/build", express.static(path.resolve(__dirname, "build")));
const listener = app.listen(process.env.PORT || 2048, () => {
console.log("Your app is listening on port " + listener.address().port);
});
App
组件使用内置的renderToNodeStream
方法进行流式渲染。
html
<!DOCTYPE html>
<html>
<head>
<title>Cat Facts</title>
<link rel="stylesheet" href="/style.css" />
<script type="module" defer src="/build/client.js"></script>
</head>
<body>
<h1>Stream Rendered Cat Facts!</h1>
<div id="approot"></div>
</body>
</html>
数据包含应用程序渲染所需的信息,例如文档标题和样式表。如果我们使用renderToString
方法在服务器端渲染App
组件,我们将需要等待应用程序接收所有数据后,才能处理这些元数据。为了加快速度,renderToNodeStream
使应用程序可以在接收来自App
组件的数据块的同时,加载和处理这些信息!
概念
与渐进式水合类似,流式渲染是另一种可以用来提高服务器端渲染(SSR)性能的机制。顾名思义,流式渲染意味着从服务器发送到客户端的HTML是流式传输的。由于客户端可以更早地接收HTML,页面首次字节时间(TTFB)会减少。由于渲染是渐进式的,因此首次绘制(FP)和首次内容绘制(FCP)的时间也会更短。
React的流式渲染支持
React在2016年发布的React 16中引入了对流式渲染的支持。以下API支持流式渲染。
ReactDOMServer.renderToNodeStream(element)
:
该函数的输出与ReactDOMServer.renderToString(element)
相同,以Node.js可读流格式返回,而不是字符串。该函数仅在服务器端用于流式方式渲染HTML。接收的客户端可以调用ReactDOM.hydrate()来水合页面。ReactDOMServer.renderToStaticNodeStream(element)
:
可用于在服务器端渲染静态、非交互式页面,然后将其流式传输到客户端。
将所有内容整合在一起,现在让我们看看这个代码:
jsx
import { renderToNodeStream } from 'react-dom/server';
import Frontend from '../client';
app.use('*', (request, response) => {
// Send the start of your HTML to the browser
response.write('<html><head><title>Page</title></head><body><div id="root">');
// Render your frontend to a stream and pipe it to the response
const stream = renderToNodeStream(<Frontend />);
stream.pipe(response, { end: 'false' });
// Tell the stream not to automatically end the response when the renderer finishes.
// When React finishes rendering send the rest of your HTML to the browser
stream.on('end', () => {
response.end('</div></body></html>');
});
});
SSR与流式渲染的绘制的对比:
流式SSR的优缺点
流式渲染旨在提高React的服务器端渲染性能,并提供以下好处:
- 性能提升:由于第一个字符在服务器端渲染开始后不久即可到达客户端,因此TTFB比SSR更好。由于客户端可以在接收到数据后立即开始解析HTML,因此首次绘制(FP)和首次内容绘制(FCP)的时间也更短。
- 处理背压:流式渲染对网络背压或拥塞响应良好。
- 支持SEO:流式传输的响应可以被搜索引擎爬虫读取,从而允许网站进行SEO优化。
需要注意的是,流式渲染的实现并不是简单的将renderToString
替换为renderToNodeStream()
。有些情况下,与SSR工作的代码可能无法直接与流式渲染一起使用。
以下是迁移可能的几个示例。
- 在服务器渲染过程中生成需要添加到文档中的框架。例如,动态添加到页面中的CSS的框架,或在渲染时向文档
<head>
中添加元素的框架。这里讨论了一种解决方法。 - 使用
renderToStaticMarkup
生成页面模板并在其中嵌入renderToString
调用以生成动态内容的代码。由于在这种情况下期望组件对应的字符串,因此不能用流替换。这里提供了一个这样的代码示例。
jsx
res.write("<!DOCTYPE html>");
res.write(renderToStaticMarkup(
<html>
<head>
<title>My Page</title>
</head>
<body>
<div id="content">
{ renderToString(<MyPage/>) }
</div>
</body>
</html>);
流式渲染和渐进式水合都可以帮助弥合服务器端渲染和客户端渲染之间的差距。