JS `Streaming SSR`:渐进式渲染与更快的首屏内容

各位观众老爷,大家好!我是你们的老朋友,今天咱来聊聊一个听起来高端大气上档次,实际上原理简单易懂的东西——JS Streaming SSR,也就是流式SSR。

什么?你说你已经听说过SSR了?那敢情好,省的我从头开始科普了。不过,普通的SSR和Streaming SSR,那可不是一回事儿。就好比都是吃饭,一个是大锅饭,一个自助餐,想吃啥拿啥,效率杠杠的!

传统的SSR的痛点

先来回顾一下传统的SSR。简单来说,就是服务器把整个页面都渲染好,然后一股脑儿地发给浏览器。

  • 问题一:TTFB(Time To First Byte)太长。服务器得吭哧吭哧地把所有数据都准备好,然后才能开始发送,这段时间用户只能干瞪眼。
  • 问题二:阻塞渲染。浏览器收到完整的HTML后才能开始解析,然后才能渲染页面,用户体验大打折扣。

想象一下,你点了个外卖,商家非得把所有菜都炒好,装盒,打包,再送到你手里,等你饿得前胸贴后背了,才能吃上一口热乎饭。

Streaming SSR:化整为零,逐段发送

Streaming SSR的核心思想就是:化整为零,逐段发送。服务器不再等待整个页面渲染完成,而是将页面分成多个小块,渲染完一块就发送一块。

  • 优势一:更快的TTFB。服务器只要渲染出页面的一部分就可以开始发送,用户可以更快地看到内容。
  • 优势二:非阻塞渲染。浏览器收到一部分HTML就可以开始解析和渲染,用户体验大大提升。

还是外卖的例子,Streaming SSR就像是你点了个盖饭,商家先给你盛好米饭,然后陆陆续续把菜盖上去,你不用等到所有菜都炒好,就可以先吃米饭了。

实现Streaming SSR的技术方案

目前主流的实现方案主要有以下几种:

  1. React的renderToPipeableStreamrenderToReadableStream

    React 18 引入了 renderToPipeableStreamrenderToReadableStream,专门用于实现Streaming SSR。前者用于Node.js环境,后者用于Web Streams API环境(例如Deno或Cloudflare Workers)。

  2. Vue的@vue/server-renderer

    Vue官方提供了 @vue/server-renderer 包,也支持Streaming SSR。

  3. Next.js 的 Server Components (React Server Components)

    Next.js 利用 React Server Components,可以实现更细粒度的Streaming SSR,甚至可以在组件级别进行流式渲染。

接下来,我们以React的renderToPipeableStream为例,来演示如何实现Streaming SSR。

React Streaming SSR 代码示例

首先,我们需要安装React 18:

npm install react react-dom

然后,创建一个简单的React组件:

// App.jsx
import React, { Suspense } from 'react';

const Header = React.lazy(() => import('./Header'));
const Content = React.lazy(() => import('./Content'));
const Footer = React.lazy(() => import('./Footer'));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading Header...</div>}>
        <Header />
      </Suspense>
      <Suspense fallback={<div>Loading Content...</div>}>
        <Content />
      </Suspense>
      <Suspense fallback={<div>Loading Footer...</div>}>
        <Footer />
      </Suspense>
    </div>
  );
}

export default App;

这里我们使用了React.lazySuspense 来实现代码分割和延迟加载,这可以进一步提升Streaming SSR的效率。

再创建Header.jsxContent.jsxFooter.jsx组件:

// Header.jsx
import React from 'react';

function Header() {
  return (
    <header>
      <h1>Welcome to My Streaming SSR App</h1>
    </header>
  );
}

export default Header;

// Content.jsx
import React from 'react';

function Content() {
  return (
    <main>
      <p>This is the main content of the page.</p>
    </main>
  );
}

export default Content;

// Footer.jsx
import React from 'react';

function Footer() {
  return (
    <footer>
      <p>&copy; 2023 My App</p>
    </footer>
  );
}

export default Footer;

接下来,创建一个Node.js服务器,使用renderToPipeableStream来渲染React组件:

// server.js
import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';

const app = express();
const port = 3000;

app.use(express.static('public')); // Serve static files

app.get('/', (req, res) => {
  res.setHeader('Content-Type', 'text/html');

  let didError = false;

  const { pipe, abort } = renderToPipeableStream(
    <App />,
    {
      bootstrapModules: ['/client.js'], // client side entry point
      onShellReady() {
        // If something errored before the root layout, we set the error code.
        res.statusCode = didError ? 500 : 200;
        pipe(res);
      },
      onError(err) {
        didError = true;
        console.error(err);
      }
    }
  );

  setTimeout(abort, 10000); // In case of error, abort after 10 seconds.
});

app.listen(port, () => {
  console.log(`Server listening on port ${port}`);
});

这段代码中,renderToPipeableStream 接受两个参数:

  • 第一个参数:要渲染的React组件。
  • 第二个参数:一个配置对象,包含以下几个重要的选项:
    • bootstrapModules:客户端入口文件,用于hydration。
    • onShellReady:当页面的“壳”渲染完成时调用,表示可以开始向客户端发送数据了。
    • onError:发生错误时调用。

最后,创建一个客户端入口文件client.js,用于hydration:

// client.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.hydrateRoot(document, <App />);

别忘了在你的 HTML 模板中包含客户端入口文件:

<!DOCTYPE html>
<html>
  <head>
    <title>Streaming SSR Example</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="/client.js" async></script>
  </body>
</html>

启动服务器:

node server.js

在浏览器中访问 http://localhost:3000,你就可以看到Streaming SSR的效果了。

Streaming SSR 的优势与挑战

优势 挑战
更快的TTFB,提升用户体验 需要更复杂的服务器端代码
非阻塞渲染,更快地显示首屏内容 需要处理错误和超时
更好的SEO,搜索引擎可以更快地抓取内容 需要考虑hydration的问题,确保客户端和服务端渲染结果一致
可以更好地利用资源,避免服务器长时间阻塞 对于复杂的应用,需要更仔细地设计组件结构,以便更好地进行流式渲染

Streaming SSR 的使用场景

  • 电商网站:可以更快地显示商品列表和详情,提升用户购物体验。
  • 新闻网站:可以更快地显示新闻内容,吸引用户阅读。
  • 博客网站:可以更快地显示文章内容,提高用户阅读量。
  • 任何需要快速首屏渲染的网站

总结

Streaming SSR是一种非常有用的技术,可以显著提升网站的性能和用户体验。虽然实现起来稍微复杂一些,但是带来的收益是巨大的。

希望今天的讲座对大家有所帮助。记住,技术是为人类服务的,不要被技术吓倒,要勇敢地拥抱新技术,让我们的网站更快、更强大!

谢谢大家!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注