服务端渲染 SSR 原理和实现

CSR 优缺点

优点

  • 整个网站打包进 JavaScript 里,当 JavaScript 下载完毕后,相当于网站的页面资源都被下载好了。这样在跳转新页面的时候,不需要向服务器再次请求资源( JavaScript 会直接操作 DOM 进行页面渲染),从而让整个网站的使用体验上更加流畅

缺点

  • 在 JavaScript 体积较大的情况下,会有白屏问题
  • 因为会先下载一个空的 HTML,然后才通过 JavaScript 进行渲染,这个空的 HTML 会导致某些搜索引擎无法通过爬虫正确获取网站信息,从而影响网站的搜索引擎排名

SSR

优点

  • HTML 在服务器端就已经渲染好了,浏览器拿到就可以渲染,减少白屏时间
  • 服务器端渲染拥有良好的首屏性能和 SEO

缺点

  • 每次跳转页面都要向服务器重新请求,意味着用户每次切换页面都要等待一小段时间
  • SSR 相比 CSR 会占用较多的服务器端资源
  • 针对于 CSR 的方式它是一种纯静态资源。我们可以直接将它放在 CDN 上就可以良好的用户访问到,而 SSR 的方式必须依赖于一个服务器进行服务端预渲染。(当然纯 SSG 应用不在这个讨论范围之内)
  • Time to Interactive 可交互时间 (TTI) 的增长,虽然说 SSR 的方式有效的缩短了首屏加载的方式,但是会增加所谓的TTI(可交互时间),因为需要去做 Hydrate 水合
// 以 vue 为例
import { renderToString } from 'vue/server-renderer'
import { createSSRApp } from 'vue'

// 一个计数的 vue 组件
function createApp() {
  // 通过 createSSRApp 创建一个vue实例
  return createSSRApp({
    data: () => ({ count: 1 }),
    template: `<button @click="count++">{{ count }}</button>`,
  });
}

const app = createApp();

// 通过 renderToString 将 vue 实例渲染成字符串
renderToString(app).then((html) => {
  // 将字符串插入到 html 模板中
  const htmlStr = `
    <!DOCTYPE html>
    <html>
      <head>
        <title>Vue SSR Example</title>
      </head>
      <body>
        <div id="app">${html}</div>
      </body>
    </html>
  `;
  console.log(htmlStr);
});

renderToString 将虚拟 DOM 转换成 HTML 字符串

// 假设这是我们的简化的虚拟 DOM 节点构造函数
function VNode(tag, data, children) {
  this.tag = tag;
  this.data = data;
  this.children = children;
}

// 一个简化的渲染函数,它接受一个虚拟 DOM 节点并返回一个 HTML 字符串
function renderNode(vNode) {
  let html = '';

  // 如果是文本节点
  if (!vNode.tag) {
    return vNode.children;
  }

  // 开始标签
  html += `<${vNode.tag}`;

  // 添加属性
  if (vNode.data && vNode.data.attrs) {
    for (const [key, value] of Object.entries(vNode.data.attrs)) {
      html += ` ${key}="${value}"`;
    }
  }
  html += '>';

  // 渲染子节点
  if (vNode.children) {
    vNode.children.forEach(child => {
      html += renderNode(child instanceof VNode ? child : new VNode(null, null, child));
    });
  }

  // 结束标签
  html += `</${vNode.tag}>`;

  return html;
}

// renderToString 函数,它接受一个 Vue 实例并返回一个 HTML 字符串
function renderToString(vueInstance) {
  // 假设我们的 Vue 实例有一个 render 方法来生成虚拟 DOM
  const vNode = vueInstance.render();
  // 使用 renderNode 方法将虚拟 DOM 转换为 HTML 字符串
  return renderNode(vNode);
}

// 模拟 Vue 实例
const vueInstance = {
  render() {
    // 创建一个虚拟 DOM
    return new VNode(
      'div',
      { attrs: { id: 'app' } },
      [
        new VNode('h1', null, ['Hello, Vue SSR']),
        new VNode('p', null, ['This is a server-rendered message.'])
      ]
    );
  }
};

// 使用 renderToString 函数渲染 Vue 实例为 HTML 字符串
const html = renderToString(vueInstance);

Server + Client 同构

  • 开始的步骤和 SSR 相同,将生成的 HTML 字符串返回给客户端,同时将 CSR 需要的 JavaScript 也一并发送给客户端
  • 客户端在接收到 SSR 生成的 HTML 后,页面还会再执行一次 CSR 的流程
  • 客户端只有请求的第一个页面是在服务器端渲染的,其它页面则都是在客户端进行的
  • 这样就同时兼顾首屏、SEO和用户体验的网站
    在这里插入图片描述

Hydrate 水合(客户端激活)

  • 服务器执行应用的初始渲染,生成静态 HTML,并将其发送给客户端,这一步其实发送的是静态的模版( Dehydrate 脱水)
  • 客户端加载额外的 JavaScript 代码,并在已有的静态 HTML 上绑定事件监听器等,使页面变得可交互
  • SSR 的瓶颈也就取决于 Hydrate 的过程

数据的获取和初始化

  • 挂载到 window 上或者 Vuex、Pinia 等其它方案
const htmlStr = `
  <!DOCTYPE html>
  <html>
    <head>
      ...
      // 将数据格式化成json字符串,放到script标签中
      <script>window.__INITIAL_DATA__ = ${JSON.stringify(initData)}</script>
    </head>
    ...
  </html>
`;

组件中获取数据

function createApp() {
  return createSSRApp({
    data: () => ({ count: 1 }),
    template: `<button @click="count++">{{ count }}</button>`,
    // 自定义一个名为 asyncData 的函数
    asyncData: async () => { 
        // 在处理远程数据并 return 出去
        const data = await getSomeData()
        return data; 
    },
    async mounted() {
      // 如果已经有数据了,直接从 window 中获取
      if (window.__INITIAL_DATA__) {
        // 有服务端数据时,使用服务端渲染时的数据
        this.count = window.__INITIAL_DATA__;
        window.__INITIAL_DATA__ = undefined;
        return;
      } else {
        // 如果没有数据,就请求数据
        this.count = await getSomeData();
      }
    }
  });
}

SSR 相关优化

预加载资源

  • 在打包过程中生成 manifest
    • 作用是将打包后的模块 ID 与它们关联的 Chunk 和资源文件进行映射
  • 依靠这个 manifest 获取资源的路径,然后创建 Link 标签拼接到 HTML 模板中即可

避免应用单例

  • 服务器端返回给客户端的每个请求都应该是全新的、独立的应用程序实例,避免直接将对象或变量创建在全局作用域,否则它将在所有请求之间共享,在不同请求之间造成状态污染

避免全局副作用代码

  • 比如 vue 服务器端渲染只会执行 beforeCreate 和 created 生命周期,应该避免在这两个生命周期里产生全局副作用的代码
  • 例如使用 setInterval 设置定时器。在纯客户端的代码中,我们可以设置一个定时器,然后在 beforeDestroy 或 destroyed 生命周期中将其销毁。但是在 SSR 期间并不会调用销毁钩子函数,所以定时器将永远保留下来,最终造成服务器内存溢出

Server Component

  • 通过 React 新的 API Server Component 可以实现流式渲染,也就是说可以按照组件维度,一小部分一小部分渲染,缩短单次 Hydrate 时间

Qwik 省略 Hydrate(惰性 Hydrate + 映射)

  • 在 SSR 中,组件在首次渲染后,还需要同构才能变得可交互,同一个组件的渲染逻辑被执行了两遍
  • Qwik 中提出了一个全新的思路来规避 RECOVERY 带来的外开销
    • 将所有必需的信息序列化为 HTML 的一部分,然后一起下发
      • Qwik 将需要的状态以及事件序列化保存在 Server 端下发的 HTML 模版中,需要序列化信息需要包括WHAT(事件处理函数内容), WHERE(哪些节点需要哪些类型的事件处理函数), APP_STATE(应用状态), 和FRAMEWORK_STATE(框架状态)
      • 依赖于事件冒泡来拦截所有事件的全局事件处理程序
      • Qwki 内部存在一个可以延迟恢复事件处理程序的工厂函数
    • Qwik 中事件处理程序是在全局处理的,这样我们就不必在在特定的 DOM 元素上单独注册所有事件
  • 对比传统的 Hydration 方案,在客户端获得服务端下发的 HTML 后会立即请求需要的 JS 脚本并执行从而为页面附加对应的交互效果,Qwik 提出的概念恰恰相反,获取完服务端下发的 HTML 页面后所有的交互效果实际上都是一种惰性创建的效果
  • 在 HTML 中的每个元素中都已经通过序列化从而在它的标签属性上记录了对应事件处理函数的位置以及脚本内容(自然内容中也包含对应的状态),所以当获得 HTML 页面后其实就可以说此时页面已经加载完毕了而不需要任何实时的 JS 执行
  • Qwik 编译后的模版大概如下
<div q:host>
  <div q:host>
    <button on:click="./chunk-a.js#button">Trip Biz</button>
  </div>
  <div q:host>
    <button q:obj="1" on:click="./chunk-b.js#count[0]">10</button>
  </div>
</div>
<script id="qwikloader"> /* qwik 中设置全局事件监听器的代码 */</script>
<script id="qwik/json"> /* 用于反序列化的 JSON 相关信息 */</script>

在这里插入图片描述

  • 不足
    • Qwik 中选择在触发用户行为时,再惰性加载并执行响应的 JS 脚本。那么难免需要在用户触发交互时动态生成对应的事件处理函数进行执行
      • 解决方案通过 prefetch 进行优化,优先下载重要的 JavaScript 脚本
    • 延迟加载模块的确会存在多个 small bundle 的问题
      • Qwik 中提供支持将多个小的 chunk 自由组合成为一个从而有效的减少细碎 chunk 的数量的方式

Astro SSG 方式

  • 待补充
  • 23
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值