首页 > 编程语言 >Node.js 实现流式渲染

Node.js 实现流式渲染

时间:2024-09-12 10:51:47浏览次数:3  
标签:Node const 渲染 rs 流式 js async new

什么是流式渲染?

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

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

Node.js 实现简单流式渲染

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

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

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[2] 的技巧!众多组件化设计的前端框架都包含了 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 是一个相对较新的特性,您可以在这篇文档[3]中获取更多详细信息。

react 实现流式渲染

自 React 18 版本之后,在框架层面上开始支持流式渲染。下面是使用 Suspense对之前的示例进行改写的代码:

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 代码。

总结

本文从理论层面深入探讨了流式渲染的相关实现方案。理论上,流式渲染的概念和实现相对简单。HTTP 标准和 Node.js 早在很久以前就对这一特性提供了支持。然而,在实际的工程应用中,流式渲染并非易事。以 React 为例,要实现流式渲染,不仅需要 React 自身作为用户界面(UI)框架提供支持,还需要借助像 nextjs 这样的元框架(meta framework)来赋予服务端相应的能力。

标签:Node,const,渲染,rs,流式,js,async,new
From: https://blog.csdn.net/lbking666666/article/details/142132391

相关文章

  • vite如何打包vue3插件为JSSDK
    安装vitenpmcreatevite@latest你还可以通过附加的命令行选项直接指定项目名称和你想要使用的模板。例如,要构建一个Vite+Vue+ts项目,运行:#npm7+,需要添加额外的--:npmcreatevite@latestmy-vue-app----templatevue-ts查看create-vite以获取每个模板的更多细......
  • uniapp js 划消小游戏 2.0
    1.效果图  代码:game.vue  <template> <viewclass="wrap">  <!--<imgclass="imgBG"src="@/static/image/hxBG.png"alt=""/>-->  <imageclass="imgBG"mode="aspectFill&q......
  • js代理函数
    consthook=true,compress=truedeleteprocessdeleteglobaldeleterequiredeletemoduledeleteBufferdelete__dirnamedelete__file__consthook_funcs=['toString','hasOwnProperty']constconstructor_excepts=[Date,RegExp]......
  • js简介
    js简介js出生的时候是为了解决表单数据的合法性验证JavaScript:简写js,但是他与Java没有半毛钱关系js可以控制web前端技术的钱两者:html(结构)和css(样式)js基础语法ctrl+?依旧是注释的快捷键,只不过在不同语言的语法显示不一样alert的作用是弹出对话框,小括号中的内容可以提示文字......
  • JsonConfigurationFileParser
    internalclassProgram{staticasyncTaskMain(string[]args){varroot=newRoot{Demo1=newDemo1{Name="Demo1",Data=newDemo2{Name="Demo2"......
  • JS获取URL参数的几种方法
    JS获取URL参数的几种方法在Web开发中,经常需要从URL中提取参数来进行相应的操作。本文将深度解析在JavaScript中获取URL参数的几种方法,并附带一些扩展与高级技巧。希望对你有所帮助!一、JS获取URL参数包含哪些方式1.使用URL对象现......
  • fastjson1.2.24反序列化漏洞复现 CVE-2017-18349
    1.准备:1.1复现环境漏洞环境:vulnhub靶场工具准备:jdk8,apache-maven-3.9.9,kali2024.1,MarshalSec1.2环境启动进入vulnhub目录下的fastjson目录,进入CVE-2017-18349目录cd/home/hbesljx/vulhub/fastjson/1.2.24-rcedocker-compoe启动漏洞环境docker-composeup-d访问靶机......
  • Threejs之光线投射Raycaster
    本文目录前言一、简要介绍1.1定义与原理1.2构造器1.3常用属性1.4常用方法二、代码准备及效果2.1演示代码准备2.2效果三、创建射线Raycaster及效果3.1代码3.2效果四、完整代码前言Three.js中的光线投射(Raycaster)是一个功能强大的类,用于在三维场景中执行射......
  • Threejs之光线投射Raycaster交互
    这里写目录标题前言一、前置准备1.1代码1.2效果二、添加交互事件2.1代码2.2效果三、完整代码前言基于上篇文章Threejs之光线投射Raycaster我们知道了光线投射的基础用法,在本届我们将使用光线投射进行鼠标交互事件一、前置准备1.1代码<!DOCTYPEhtml><ht......
  • 面试-JS Web API-DOM
    概览DOM(DocumentObjectModel)DOM是哪种数据结构?---树......