如何使用流式渲染技术提升用户体验

什么是流式渲染?

流式渲染主要思想是将HTML文档分块(chunk)并逐块发送到客户端,而不是等待整个页面完全生成后再发送。

流式渲染不是什么新鲜的技术。早在90年代,网页浏览器就已经开始使用这种方式来处理HTML文档。

在 SPA (单页应用)流行的时代,由于 SPA 的核心是客户端动态地渲染内容,流式渲染没有得到太多关注。如今,随着服务端渲染相关技术的成熟,流式渲染成为可以显著提升首屏加载性能的利器。

下面这个 gif 可以直观地看到不使用流式渲染和使用流式渲染的差异:

素材来源于文章

Node.js 实现简单流式渲染

HTTP is a first-class citizen in Node.js, designed with streaming and low latency in mind. This makes Node.js well suited for the foundation of a web library or framework.

HTTP 是 Node.js 中的一等公民,其设计时考虑到了流式传输和低延迟。这使得 Node.js 非常适合作为 Web 库或框架的基础。

------------ Node.js官网

Node.js 在设计之初就考虑到了流式传输数据,考虑如下代码:

js 复制代码
const Koa = require('koa');
const app = new Koa();

// 假设数据需要 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 或者骨架屏之类的东西来改善用户体验。查看改进后的代码:

js 复制代码
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"。

需要注意的是: Safari 浏览器对于何时触发流式传输可能有一些限制(以下内容未找到官方说明,通过实践总结得到):

  • 传输的 chunk 需大于 512 字节
  • 传输的内容需能够在屏幕上实际渲染,例如传输 <div style="display:none;">...</div> 可能是不生效的。

声明式 Shadow DOM,不依赖 javascript 实现

上面的代码中,我们用到了一些 javascript,本质上我们需要预先渲染一部分 html 标签作为占位,之后在用新的 html 标签去替换他们。这用 javascript 很好实现,如果我们禁用了 javascript 呢?

这可能需要一些 Shadow DOM 的技巧!很多组件化设计前端框架都有 slot 的概念,在 Shadow DOM 中也提供了 slot 标签,可以用于创建可插入的 Web Components。 在 chrome 111 版本以上,我们可以使用声明式 Shadow DOM,不依赖 javascript,在服务器端使用 shadow DOM。 一个声明式 Shadow DOM 的例子:

html 复制代码
    <template shadowrootmode="open">
      <header>Header</header>
      <main>
        <slot name="hole"></slot>
      </main>
      <footer>Footer</footer>
    </template>
  
    <div slot="hole">插入一段文字!</div>

渲染结果如下:

可以看到,我们的文字被插入在了 slot 标签中,利用声明式 Shadow DOM,我们可以改写上面的例子:

js 复制代码
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(`
  <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 是一个比较新的特性,可以在这篇文档中看到更多内容。

react 实现流式渲染

我们换个视角看看 react,react 18 之后在框架层面上支持了流式渲染, 下面是使用 nextjs 改写上面的代码:

jsx 复制代码
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 的流式渲染在这里能看到官方技术层面上的解释。本文作为对于流式渲染的概览,不作更细致的讲解。

总结

本文从理论上探讨了流式渲染相关实现方案,理论上,流式渲染很简单。HTTP 标准和 Node.js 很早之前就支持了这一特性。但在工程实践中,它很复杂。例如对于 react 来说,流式渲染不仅仅需要 react 作为 UI 来支持,也需要借助 nextjs 这种元框架(meta framework)提供服务端的能力。

感谢阅读,欢迎交流~

相关推荐
丁总学Java3 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
看到请催我学习5 小时前
如何实现两个标签页之间的通信
javascript·css·typescript·node.js·html5
zqx_78 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
NiNg_1_2349 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
余生H10 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
Ink11 小时前
从底层看 path.resolve 实现
前端·node.js
奔跑吧邓邓子13 小时前
npm包管理深度探索:从基础到进阶全面教程!
前端·npm·node.js
知否技术1 天前
为什么nodejs成为后端开发者的新宠?
前端·后端·node.js
TonyH20021 天前
webpack 4 的 30 个步骤构建 react 开发环境
前端·css·react.js·webpack·postcss·打包