聊聊流式渲染

流式渲染其实不是什么新鲜事,近年随着 ssr 越来越多的使用,这个概念又重新被大众熟悉,这篇文章就介绍下目前主流的流式渲染方案和 slot 方案。

什么是流式渲染

在了解流式渲染之前,我们要对了解要了解浏览器怎么处理 html 的。浏览器的渲染过程我就不介绍了,不清楚的可以看下这篇文章

浏览器容错机制

首先,浏览器的容错机制是很强的,你能想得到的问题浏览器几乎都能处理,例如下面这个 html,我们就算没写闭合标签,浏览器也能正常解析。

css 复制代码
<div>
  hello
  <div>world

浏览器边加载边渲染

浏览器不会等 html 加载完才会渲染,它会尽快将 html 渲染上去,甚至在加载完之前就会渲染到页面上,如果我们将 html 分块传输,浏览器是可以先把加载的分块渲染到页面上的。

我们用 node 做个 demo,可以看到loading 会先显示出来,然后done才会显示。

javascript 复制代码
import http from "http";

const server = http.createServer(async (req, res) => {
  res.statusCode = 200;
  res.setHeader("Content-Type", "text/html");
  res.write("<div>loading...</div>");
  await new Promise((resolve) => setTimeout(resolve, 1000));
  res.write("<div>done</div>");
  res.end();
});
console.log("Server running at http://localhost:8080/");
server.listen(8080);

优点

一个方案总是要解决一些问题的,从上面例子中你也能一窥一二,简单来说,流式渲染的优点有:

  1. 更快的初始加载时间,不需要等待整个页面加载完成
  2. 改善搜索引擎优化,虽然 Google 等一些搜索引擎已经可以处理 js 渲染的页面,但是流式 SSR 有助于提高搜索引擎对网站的可见性和排名
  3. 更好的用户体验,用户可以更早地开始与页面进行交互
  4. 更容易实现首屏渲染,网站可以更快地展示重要的内容给用户

实现方案

我们先介绍下目前大部分框架的流式渲染方案,然后再介绍下一个 slot 的方案

主流方案

现在主流的方案其实很简单,先把处理完的 html 传给浏览器,对于一些要覆盖的地方,可以在后续返回 script 标签并执行替换。

这里以 react 的renderToPipeableStream为例,我们可以搭个 ssr 的 demo 看一下,具体代码就不在这里描述了,唯一值得注意的是为了更长时间触发Suspense,我在Post.jsx抛出了一个 Promise 错误,详情可以查看这里

最后生成的的 html 如下:

html 复制代码
<!doctype html>
<html>
  <body id="app">
    <!--$?-->
    <template id="B:0"></template>
    <div>Loading...</div>
    <!--/$-->
  </body>
</html>
<div hidden id="S:0">
  <div>this is a post</div>
</div>
<script>
  function $RC(a, b) {
    a = document.getElementById(a);
    b = document.getElementById(b);
    b.parentNode.removeChild(b);
    if (a) {
      a = a.previousSibling;
      var f = a.parentNode,
        c = a.nextSibling,
        e = 0;
      do {
        if (c && 8 === c.nodeType) {
          var d = c.data;
          if ("/$" === d)
            if (0 === e) break;
            else e--;
          else ("$" !== d && "$?" !== d && "$!" !== d) || e++;
        }
        d = c.nextSibling;
        f.removeChild(c);
        c = d;
      } while (c);
      for (; b.firstChild; ) f.insertBefore(b.firstChild, c);
      a.data = "$";
      a._reactRetry && a._reactRetry();
    }
  }
  $RC("B:0", "S:0");
</script>

整体逻辑不难理解,react 用<!--$?--><!--/$-->注释节点(nodeType==8)标记要替换的节点,然后$RC方法将注释内的节点完全移除,并将S:0的内容做替换。这里也可以看到浏览器的容错机制,即使在 html 标签外面也能正常解析。

我们改造下前面的 demo,让它支持替换 loading 元素,这里用原生replaceWith方法代替

javascript 复制代码
import http from "http";

const server = http.createServer(async (req, res) => {
  res.statusCode = 200;
  res.setHeader("Content-Type", "text/html");
  res.write(`<div id="loading">loading</div>`);
  await new Promise((resolve) => setTimeout(resolve, 1000));
  res.write(`<div id="done">done</div>`);
  res.write(`<script>document.getElementById("loading").replaceWith(document.getElementById("done"));</script>`);
  res.end();
});
console.log("Server running at http://localhost:8080/");
server.listen(8080);

slot 方案

虽然上面方案在体验上能满足需求,但是还是有些问题的,比如:seo 不友好,虽然流式渲染比传统的单页应用更容易被搜索引擎爬取,但不稳定的结构还是会影响 seo,另外借助 js 的替换也可能有性能问题。

哪有没有原生的方式做到同样的效果呢?答案是有的,我们可以使用slot来实现,slot 是 html5 的新特性,可以让我们在 template 中定义一些占位符,然后在后续的 html 中替换这些占位符。不过 slot 也有一些限制,它只能在 template 中使用。

html 复制代码
<template shadowrootmode="open">
  <slot name="content">loading</slot>
</template>
<div slot="content">done</div>

上面这段代码会渲染成done

继续改造下前面的 demo,让它支持 slot 替换 loading 元素

javascript 复制代码
import http from "http";

const server = http.createServer(async (req, res) => {
  res.statusCode = 200;
  res.setHeader("Content-Type", "text/html");
  res.write(`<body>
  <template shadowrootmode="open">
    <slot name="content">loading</slot>
  </template>
</body>`);
  await new Promise((resolve) => setTimeout(resolve, 1000));
  res.write(`<div slot="content">done</div>`);
  res.end();
});
console.log("Server running at http://localhost:8080/");
server.listen(8080);

不过这个方案依赖于Declarative Shadow DOM,支持非常有限。

相关推荐
qq_3901617718 分钟前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test1 小时前
js下载excel示例demo
前端·javascript·excel
Yaml41 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶1 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v1 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫1 小时前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web
贩卖纯净水.1 小时前
Chrome调试工具(查看CSS属性)
前端·chrome
栈老师不回家2 小时前
Vue 计算属性和监听器
前端·javascript·vue.js