聊聊流式渲染

流式渲染其实不是什么新鲜事,近年随着 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,支持非常有限。

相关推荐
彩虹下面1 小时前
手把手带你阅读vue2源码
前端·javascript·vue.js
华洛1 小时前
经验贴:Agent实战落地踩坑六大经验教训,保姆教程。
前端·javascript·产品
luckyzlb2 小时前
03-node.js & webpack
前端·webpack·node.js
左耳咚2 小时前
如何解析 zip 文件
前端·javascript·面试
程序员小寒2 小时前
前端高频面试题之Vue(初、中级篇)
前端·javascript·vue.js
陈辛chenxin2 小时前
软件测试大赛Web测试赛道工程化ai提示词大全
前端·可用性测试·测试覆盖率
沿着路走到底2 小时前
python 判断与循环
java·前端·python
Code知行合壹2 小时前
AJAX和Promise
前端·ajax
大菠萝学姐2 小时前
基于springboot的旅游攻略网站设计与实现
前端·javascript·vue.js·spring boot·后端·spring·旅游
心随雨下3 小时前
TypeScript中extends与implements的区别
前端·javascript·typescript