const Koa = require('koa'); const app = new Koa(); // 堆代码 duidaima.com // 假设数据需要 5 秒的时间来获取 renderAsyncString = async () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve('<h1>Hello World</h1>'); }, 5000); }) } app.use(async (ctx, next) => { ctx.type = 'html'; ctx.body = await renderAsyncString(); await next(); }); app.listen(3000, () => { console.log('App is listening on port 3000'); });这是一个简化的业务场景,运行起来后,会在5秒的白屏后显示一段 hello world 文字。没有用户会喜欢一个会白屏5秒的网页!在 web.dev 对 TTFB 的介绍中,加载第一个字节的时间应该在 800ms 内才是良好的 web 网站服务。我们可以利用流式渲染技术来改善这一点,先通过渲染一个 loading 或者骨架屏之类的东西来改善用户体验。查看改进后的代码:
const Koa = require('koa'); const app = new Koa(); const Stream = require('stream'); // 假设数据需要 5 秒的时间来获取 renderAsyncString = async () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve('<h1>Hello World</h1>'); }, 5000); }) } app.use(async (ctx, next) => { const rs = new Stream.Readable(); rs._read = () => {}; ctx.type = 'html'; rs.push('<h1>loading...</h1>'); ctx.body = rs; renderAsyncString().then((string) => { rs.push(`<script> document.querySelector('h1').innerHTML = '${string}'; </script>`); }) }); app.listen(3000, () => { console.log('App is listening on port 3000'); });使用流式渲染后,这个页面最初显示 "loading...",然后在 5 秒后更新为 "Hello World"。
传输的内容需能够在屏幕上实际渲染,例如传输 <div style="display:none;">...</div> 可能是不生效的。
<template shadowrootmode="open"> <header>Header</header> <main> <slot name="hole"></slot> </main> <footer>Footer</footer> </template> <div slot="hole">插入一段文字!</div>渲染结果如下:
const Koa = require('koa'); const app = new Koa(); const Stream = require('stream'); // 堆代码 duidaima.com // 假设数据需要 5 秒的时间来获取 renderAsyncString = async () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve('<h1>Hello World</h1>'); }, 5000); }) } app.use(async (ctx, next) => { const rs = new Stream.Readable(); rs._read = () => {}; ctx.type = 'html'; rs.push(` <template shadowrootmode="open"> <slot name="hole"><h1>loading</h1></slot> </template> `); ctx.body = rs; renderAsyncString().then((string) => { rs.push(`<h1 slot="hole">${string}</h1>`); rs.push(null); }) }); app.listen(3000, () => { console.log('App is listening on port 3000'); });运行这段代码,和之前的代码结果完全一致,不同的,当我们禁用掉浏览器的 javascript,代码也一样正常运行!声明式 Shadow DOM 是一个比较新的特性,可以在这篇文档中看到更多内容。
import { Suspense } from 'react' const renderAsyncString = async () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve('Hello World!'); }, 5000); }) } async function Main() { const string = await renderAsyncString(); return <h1>{string}</h1> } export default async function App() { return ( <Suspense fallback={<h1>loading...</h1>} > <Main /> </Suspense> ) }运行这段代码,和之前的代码结果完全一致,同样也不需要运行任何客户端的 javascript 代码。关于 react 的流式渲染在这里能看到官方技术层面上的解释。本文作为对于流式渲染的概览,不作更细致的讲解。