如何通过流式渲染提升用户体验?

什么是流式渲染?

流式渲染的核心理念是将 HTML 文档分割成小块(chunk),并逐步地发送给客户端,而非等待整个页面完整生成后再进行传输。这种方式能够极大地提升用户的初始加载体验,特别是在网络条件不佳或者页面内容复杂的情况下。

流式渲染并非新兴技术,早在 90 年代,网页浏览器就已开始运用这种模式来处理 HTML 文档。不过,在 SPA(单页应用)大行其道的时期,由于其核心在于客户端动态渲染内容,流式渲染未能引起广泛关注。然而,现今随着服务端渲染技术的日臻成熟,流式渲染已成为显著优化首屏加载性能的有力手段。

Node.js 实现简单流式渲染

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(Time To First Byte,首字节时间)的介绍中提到,加载第一个字节的时间应当控制在 800ms 以内,才能称得上是优质的 Web 网站服务。

为了改善这种情况,我们可以借助流式渲染技术。比如,先向用户呈现一个加载中的提示或者骨架屏,以此来优化用户体验。下面是改进后的代码:

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 'eact'

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)来赋予服务端相应的能力。

相关推荐
We་ct34 分钟前
LeetCode 125. 验证回文串:双指针解法全解析与优化
前端·算法·leetcode·typescript
帅得不敢出门1 小时前
Android Framework在mk中新增类似PRODUCT_MODEL的变量并传递给buildinfo.sh及prop属性中
android·linux·前端
小码吃趴菜1 小时前
【无标题】
前端·chrome
卓码软件测评2 小时前
第三方CNAS/CMA软件测试测评机构【LoadRunner的JSON和XML响应数据的关联和处理技巧】
测试工具·ci/cd·性能优化·单元测试·测试用例
毕设源码-朱学姐2 小时前
【开题答辩全过程】以 基于HTML5的购物网站的设计与实现为例,包含答辩的问题和答案
前端·html·html5
梦6503 小时前
CSS 元素垂直水平居中的 8 种方法
前端·css
We་ct3 小时前
LeetCode 68. 文本左右对齐:贪心算法的两种实现与深度解析
前端·算法·leetcode·typescript
ShoreKiten3 小时前
ctfshow-web316
运维·服务器·前端
前端 贾公子3 小时前
release-it 使用指南
前端·javascript
全栈技术负责人4 小时前
前端团队 AI Core Workflow:从心法到落地
前端·人工智能·状态模式