【SSR】漫谈服务端渲染

标签: ssr 服务 渲染 | 发表时间:2022-08-27 17:31 | 作者:Laffery
出处:https://juejin.cn/frontend

theme: channing-cyan

大家好,我是Laffery,本文同步发表在我的个人博客「 Kuqiochi | 谈谈服务端渲染「SSR」」。

SSR(服务端渲染,Server Side Render),顾名思义就是在服务端渲染出页面。与之相对应的是CSR(客户端渲染,Client Side Render),即在浏览器上渲染完整的页面。

Web页面渲染发展历程

在介绍SSR之前,我们先来看看历来Web页面是如何渲染的。

纯HTML时代

在网络初开的远古时期,一切皆文件,网页本质上是 托管在服务器上的HTML文件,甚至最开始是完全静态的页面,直到后来有了JavaScript和CSS,网页的交互性和内容的动态丰富性才有了保障。

模板引擎 「前后端分离」

模板引擎将视图层与数据层分离开来,是前后端分离的开端。

  1. 服务端为模板绑定数据

举个最直观的例子,flask渲染一个页面的方式就是向预设的HTML模板中传入数据,并在服务端渲染出最终的HTML(string / stream)。

flask. render_template( template_name_or_list, **context)

  render_template(
    'index.html', 
    labels=['Name', 'Description', 'Confidence'],
    records=records
)
  <table>
  <thead>
    <tr>
      {% for label in labels %}
      <th>{{ label }}</th>
      {% endfor %}
    </tr>
  </thead>
  <tbody>
    {% for record in records %}
    <tr>
      {% for part in record %}
      <td>{{ part }}</td>
      {% endfor %}
    </tr>
    {% endfor %}
  </tbody>
</table>

d2fabc7d-51ea-45ee-8045-cf2efc2767b3.png

  1. 客户端为模板绑定数据

上面仍然是一个在服务端渲染的例子,那么ejs无疑是典型的客户端渲染。在客户端渲染很显然能够减轻服务器的渲染压力。

  <script src="ejs.js"></script>
<script>
  let people = ['geddy', 'neil', 'alex'],
  html = ejs.render('<%= people.join(", "); %>', { people: people });
</script>
  1. 客户端请求数据更新视图

自从 ajax技术兴起,可以在浏览器上加载服务端数据,并且可以在不刷新页面的情况下更新视图。这时,彻底拉开了前后端分离的帷幕,前端和后端可以分开开发,解除耦合。

8fcb0cf4-c90c-40d7-9a7f-a6d0c85d4cd9.png

再后来有了我们熟知的“御三家” Angular / React / Vue,前端开发彻底繁荣。

2e4a1f72-acfb-4e44-9e4e-4482e025ead7.png

这个时候,Web应用最初从服务端获取的HTML是一个白页,依赖构建生成的 index.[hash].js在客户端执行并渲染出页面

黑魔法:CSS Database Queries

Yes, I can connect to a DB in CSS

服务端渲染

好不容易前后端分离了,在客户端渲染大家也都很受用,怎么开了历史的倒车,又放到服务端上渲染呢?

先从几个概念说起

  • **TTFB(Time to First Byte):**首个字节数据到达的时间
  • **FP(First Paint):**首次绘制的时间
  • **FMP(First Meaningful Paint):**首次有意义的内容绘制的时间
  • **TTI(Time to Interactive):**页面变得可交互的时间

服务端渲染主要针对:

  • 需要更高SEO(Search Engine Optimization),便于搜索引擎爬虫抓取网站内容的,如博客网站。

截至目前,Google 和 Bing 可以很好地对同步 JavaScript 应用进行索引。这里的“同步”是关键词。如果你的应用以一个 loading 动画开始,然后通过 Ajax 获取内容,爬虫并不会等到内容加载完成再抓取。也就是说,如果 SEO 对你的页面至关重要,而你的内容又是异步获取的,那么 SSR 可能是必需的。

  • 提高首屏渲染速度(FMP),减少白屏时间,提升用户体验。

这一点在慢网速或者运行缓慢的设备上尤为重要。服务端渲染的 HTML 无需等到所有的 JavaScript 都下载并执行完成之后才显示,所以你的用户将会更快地看到完整渲染的页面。除此之外,数据获取过程在首次访问时在服务端完成,相比于从客户端获取,可能有更快的数据库连接。这通常可以带来更高的 Core Web Vitals评分、更好的用户体验,而对于那些“首屏加载速度与转化率直接相关”的应用来说,这点可能至关重要。

  • 提升低网络和低配置设备上的性能

有些设备不支持 JavaScript 或 JavaScript 执行得很差,导致用户体验不可接受。对于这些情况,你可能会需要该应用的服务端渲染的、无 JavaScript 的版本。虽然有一些限制,不过这个版本可能是那些完全没办法使用该应用的人的唯一选择。

现在的服务端渲染主要分为两种:

  • Pre-render,在服务端预渲染;
  • Static render,在服务端渲染出最终的HTML。

预渲染的服务端渲染流程如下:

b78b12fe-73d1-487c-b3aa-2bd64a352021.png

边缘计算渲染

一方面,服务端渲染依赖中心的服务器(集群)请求动态数据并渲染HTML;另一方面,存在一个事实是服务端渲染的页面存在静态部分,没有必要重复渲染,可以考虑托管在CDN上。于是有人考虑使用边缘计算节点渲染。
简单地说, 边缘计算是将计算资源部署靠近用户和数据源的网络边缘侧,通过更靠近数据源的位置(如路由器、基站)执行计算
边缘计算渲染将需要服务端渲染的页面分为静态和动态两部分。

  • 静态部分缓存在边缘计算节点上(相当于CDN),相比传统的客户端向服务端请求,更快渲染出内容(FMP)
  • 边缘计算节点与后端服务器长连接,动态部分的数据请求无需再建立TCP连接,响应更快
  • 边缘计算节点将动态数据拼接到静态部分上

2d3c7559-7fa3-4d40-9151-d88ceb4e8a4b.png

现代SSR技术

  • CSR``Client Side Render客户端渲染
  • SSR``Server Side Render服务端渲染(✅,现在说的服务端渲染默认是上述pre render的)
  • SSG``Static Site Generate静态页面生成
logo-next logo-nuxt logo-ng-universal

基本原理

a4615133-f319-45b2-92f5-7459402e29b7.jpeg

  • 服务端根据路由找到要渲染的component
  • 服务端将页面渲染成HTML(string / stream)
  • 服务端根据component中预定义的数据预取方法请求数据
  • 服务端将数据序列化拼接到HTML中
  • 客户端接收到服务器响应,渲染收到的HTML(以及CSS等)
  • 用户可正常浏览
  • 客户端执行hydration(水化),激活页面element的事件监听方法
  • 用户可正常交互

同构

同构(Isomorphic)是SSR的核心理念。单纯实现SSR很简单,任何传统的服务端语言都能做到,但是SSR希望一套代码能在双端渲染,最大限度地重用代码,并抹除差异性,这是传统的SSR无法做到的。

logo-express logo-koa

路由同构

双端使用同一套路由规则。
服务端根据 request.url查找要渲染的组件。

  • 路由配置文件 routes.json|ts
  • 约定式路由(如根据目录结构)

数据同构

双端使用同一套数据请求方法获取数据。

  • 逻辑一致
  • 方法一致(node-fetch)

渲染同构

双端渲染出来的结果是一致的。

  • 浏览器有时会在解析HTML时自动优化,如
  <!-- server side -->
<p><div>hi</div></p>

<!-- client side -->
<p></p>
<div>hi</div>
<p></p>
  • 随机数 / hash,在双端要保持使用相同的随机数种子

React SSR demo

在React下,基于react-dom/server下的 renderToString等方法,将Component渲染成HTML string或使用 renderToXxxStream渲染成byte stream。

  import App from 'path/to/client/App'; 
import RenderDOMServer from 'render-dom/server'; 

app.get('/*', (req, res) => { 
  const html = RenderDOMServer.renderToString(<App />); 
  return res.end(html);
});

Data Fetching

renderToString是一个同步的函数,换句话说,服务端渲染的一系列API,并不能等到将组件中的异步数据加载完成后才执行渲染,所以一个直观的问题就是,前端渲染出来的HTML是没有数据的。
可以在 renderToString执行前,提前加载需要的数据,并作为参数传递给组件。

约定服务端渲染的页面,其入口文件除了默认导出的组件,还导出一个命名为 getServerSideProps的函数,用来定义数据获取的逻辑--由服务端执行。

  type AppProps = Record<string, any>;

export default App(props: AppProps) { 
    return <>...</> 
} 

export const getServerSideProps = async () => { 
    const res = await fetch('xxx'); 
    const data = await res.json() as AppProps; 
    return { props: data } 
} 

服务端在加载这个页面的时候,识别出 getServerSideProps的定义存在,则执行其数据获取逻辑:

  import RenderDOMServer from 'render-dom/server'; 
 
app.get('/*', async (req, res) => { 
    const { default: Page, getServerSideProps } = require('path/to/client/some-page'); 
     
    if (getServerSideProps) { 
        const { props } = await getServerSideProps(); 
        res.end(RenderDOMServer.renderToString(<Page {...props}/>)); 
        return; 
    } 

    res.end(RenderDOMServer.renderToString(<Page />)) 
}); 

SSR Data Context

当页面上用到了服务端获取的数据时,客户端上调用 hydrate方法后会察觉到渲染出来的差别,比如如下的SSR页面

  import HelloWorld from "@/components/hello-world"; 
 
export default function Homepage(props: { mode?: "CSR" | "SSR" }) { 
  return <div>{props.mode ?? "CSR"}</div>; 
} 
 
export const getServerSideProps = async () => { 
  return { props: { mode: "SSR" } }; 
}; 

服务端渲染的结果是“SSR”,而 hydrate又将组件渲染一遍之后页面就会变成“CSR”。
那么如何将SSR加载的数据也提供给浏览器呢,可以考虑为HTML模板添加脚本,

  <script defer type="text/javascript"> 
  (function() {
    window.SSR = true;
    window.SSR_DATA = data;
  })()
</script> 

这样在客户端执行hydrate前,从window中获取SSR_DATA对象,并为之创建上下文,为页面组件包裹一层,并在其中向页面组件注入上下文中的数据:

  import { createContext, useContext } from "react"; 
import ReactDOM from "react-dom/client"; 
import Page from "./pages/xx"; 
 
const Context = createContext({}); 
 
function App() { 
  const { props } = useContext(Context); 
  return (
    <Context.Provider value={window.SSR_DATA || {}}>
      <Page {...props} /> 
    </Context.Provider>
  );
}

ReactDOM.createRoot(rootNode).render(<App />);

这样客户端hydrate时就能渲染出和服务端一致的数据。

Hydration

客户端虽然拿到了正确的数据并拼接到HTML中,但还没有为各个element绑定事件处理函数,此时SSR页面无法响应用户的交互。
这时,在客户端React会将页面重新解析生成VDOM,为每个节点绑定事件处理方法,这样就可以像CSR一样正常的响应各个事件。传统的 render方法在首次调用时会清除SSR渲染的容器中的已有节点,而使用 hydrate方法则会对容器中的节点进行解析,并为之绑定事件监听器。

  function render() {
  const rootNode = document.getElementById("root") as HTMLElement;

  if (window && window.SSR) {
    ReactDOM.hydrateRoot(rootNode, <App />);
  } else {
    ReactDOM.createRoot(rootNode).render(<App />);
  }
}

render();

值得注意的是, hydrate会比较解析出的VDOM与SSR渲染结果,如果存在差异,则会重新渲染VDOM,退化成CSR。

约定式路由

约定式路由无需routers配置,根据路由文件结构与路径一一映射。例如Next.js的约定:

  • pages/index.js/
  • pages/blog/index.js/blog
  • pages/blog/first-post.js/blog/first-post
  • pages/dashboard/settings/username.js/dashboard/settings/username
  • pages/blog/[slug].js/blog/:slug (/blog/hello-world)
  • pages/[username]/settings.js/:username/settings (/foo/settings)
  • pages/post/[...all].js/post/* (/post/2020/id/title)

因为服务端只能根据用户请求的url确定渲染哪个页面,所以还是需要在构建时生成一个manifest文件,告诉服务端改如何找到对应的页面。

Prospect

Qwik

0bccf85f-2ba1-4523-827f-6e37d11a0801.png

  • resumable: 将序列化的数据挂在element上
  • fine-grained lazy loading:事件处理函数懒加载
  • prefetch:根据用户行为分析常用区域,优先预加载事件处理函数
  <button q:obj="1" on:click="./chunk-a.js#Counter_button_onClick[0]">
  0
</button>

React Server Components(RSC)

数据加载的三种方案:

  • waterfalls:客户端js加载完后发出大量数据请求
  • pre fetch:先请求所有数据,拼接到HTML上客户端再渲染
  • render as you fetch:不是所有的数据都是需要立刻展示的,请求优先展示的数据,剩余的放到客户端慢慢加载

组件级的服务端渲染,客户端组件不能依赖服务端组件,服务端组件使用 Suspense API包裹。在客户端,JavaScript不包含服务端渲染部分的代码,客户端组件仍会在客户端hydrate。

  import { Suspense } from 'react';

import Profile from '../components/profile.server';
import Content from '../components/content.client';

export default function Home() {
  return (
    <div>
      <h1>Welcome to React Server Components</h1>
      <Suspense fallback={'Loading...'}>
        <Profile />
      </Suspense>
      <Content />
    </div>
  )
}

参考资料

相关 [ssr 服务 渲染] 推荐:

【SSR】漫谈服务端渲染

- - 掘金 前端
大家好,我是Laffery,本文同步发表在我的个人博客「 Kuqiochi | 谈谈服务端渲染「SSR」」. SSR(服务端渲染,Server Side Render),顾名思义就是在服务端渲染出页面. 与之相对应的是CSR(客户端渲染,Client Side Render),即在浏览器上渲染完整的页面.

React服务端渲染(ssr)之Next.js框架

- - 掘金前端
Nextjs是 React生态中非常受欢迎的SSR(server side render——服务端渲染)框架,只需要几个步骤就可以搭建一个支持SSR的工程(_Nextjs_的快速搭建见 Next.js入门). 本文的案例代码来自于 前端标准模板项目. 提供了便捷强大的服务端渲染功能—— getInitialProps(),通过这个方法可以简单为服务端和前端同时处理异步请求数据:.

Google开发新服务 重写网页代码提高渲染速度

- Haides - cnBeta.COM
北京时间7月28日晚间消息,谷歌将于今天推出一种名为Page Speed Service(页面速度服务)的新服务这种服务可帮助提升网页速度. 当用户将其网站的DNS入口指向谷歌时,Page Speed Service将可从用户网站的服务器获取内容、重写网页并通过谷歌自身的全球服务器来为用户网站提供服务.

“天河一号”为国内动漫影视制作提供渲染服务

- David - cnBeta.COM
记者23日从天津滨海新区获悉,借用“云计算”技术,超级计算机“天河一号”的公共服务辐射到动漫、影视领域,成为当今世界上规模最大、渲染速度最快的渲染平台之一. 该平台实现了渲染应用与超级计算机系统的有机结合,可根据用户需求提供大型渲染业务,大大缩短了影视后期的制作时间,提升了整体后期制作水平.

当 SSR 遇上 Serverless,轻松实现页面瞬开

- -

带你五步学会Vue SSR - 前端学习 - SegmentFault 思否

- -
SSR大家肯定都不陌生,通过服务端渲染,可以优化SEO抓取,提升首页加载速度等,我在学习SSR的时候,看过很多文章,有些对我有很大的启发作用,有些就只是照搬官网文档. 通过几天的学习,我对SSR有了一些了解,也从头开始完整的配置出了SSR的开发环境,所以想通过这篇文章,总结一些经验,同时希望能够对学习SSR的朋友起到一点帮助.

ssr vuejs/vue-hackernews-2.0: HackerNews clone built with Vue 2.0, vue-router & vuex, with server-side rendering

- -
This is a demo primarily aimed at explaining how to build a server-side rendered Vue app, as a companion to our SSR documentation. #install dependenciesnpm install#or yarn#serve in dev mode, with hot reload at localhost:8080npm run dev#build for productionnpm run build#serve in production modenpm start.

从0开始微操SSR之nextjs项目实践

- - 掘金前端
这是一个电商类的网站,对于这种电商类的网站,我们一般会想要的效果是:. 1、用户从百度或者一些搜索引擎直接搜索关键字,就能够导流到我的网站上,这就是SEO(Search Engine Optimization). SEO(Search Engine Optimization):汉译为搜索引擎优化. 是一种方式:利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名.

助力ssr,使用concent为nextjs应用加点料

- - SegmentFault 最新的文章
开源不易,感谢你的支持, ❤ star concent^_^. 这里我们将使用 create-next-app命令来安装一个基础的next示例应用. 执行完毕后,可以看到一个如下的目录结构. |____public |____pages | |____ _app.js // next应用默认的根组件 | |____index.js // 默认首页 | |____api.

电信 ss/ssr 速度慢 电信国际出口速度慢 被 QoS 限速

- - DiyCode - 致力于构建开发工程师高端交流分享社区社区
很多人跟我反应,同一条线路, 电信用户的国际出口速度很慢,而移动/联通用户却还不错,可能移动/联通可以流畅看1080P,而电信卡的连国外网页都打不开. 明明电信的国际出口宽带是三家中最高的,为什么只有电信的速度慢呢. 本文简单分析下电信运行商慢的原因( QoS限速),并推荐下针对电信用户优化的CN2线路来提升国际出口速度.