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

科技   2024-11-28 08:36   北京  

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

什么是流式渲染?

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

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

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

素材来源于文章

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 在设计之初就考虑到了流式传输数据,考虑如下代码:

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

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 的例子:

    <template shadowrootmode="open">
<header>Header</header>
<main>
<slot name="hole"></slot>
</main>
<footer>Footer</footer>
</template>

<div slot="hole">插入一段文字!</div>

渲染结果如下:

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

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 改写上面的代码:

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)提供服务端的能力。

原文链接:https://juejin.cn/post/7347009547741495350

作者: 李章鱼

喜欢点赞,再看,转发谢谢!


Node 社群


我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

   “分享、点赞在看” 支持一波👍

程序员成长指北
专注 Node.js 技术栈分享,从 前端 到 Node.js 再到 后端数据库,祝您成为优秀的高级 Node.js 全栈工程师。一个有趣的且乐于分享的人。座右铭:今天未完成的,明天更不会完成。
 最新文章