Web 现代应用程序架构下的性能优化,渐进式的极致艺术。

简介: 前言本文是 Rendering on the Web: Performance Implications of Application Architecture (Google I/O ’19)[1] 这篇谷歌工程师带来的现代应用架构体系下的优化相关演讲的总结,演讲介绍了以下优化手段:• 预渲染• 同构渲染• 流式渲染• 渐进式注水(非常精彩)


前言


本文是 Rendering on the Web: Performance Implications of Application Architecture (Google I/O ’19)[1] 这篇谷歌工程师带来的现代应用架构体系下的优化相关演讲的总结,演讲介绍了以下优化手段:

  • 预渲染
  • 同构渲染
  • 流式渲染
  • 渐进式注水(非常精彩)

应用架构体系


当我们讨论「应用架构」的时候,可以理解为通过以下几个部分组合来构建网站。

  1. Component model 组件模型。
  2. Rendering and loading 渲染和加载。
  3. Routing and transitions 路由和过渡。
  4. Data/state management 数据、状态的管理。

640 (1).png

image


性能指标


在分析页面渲染性能之前,先了解一下几个比较重要的指标,方便下文理解:

  1. FP: First Paint,是 Paint Timing API 的一部分,是页面导航与浏览器将该网页的第一个像素渲染到屏幕上所用的中间时,渲染是任何与输入网页导航前的屏幕上的内容不同的内容。
  2. FCP: First Contentful Paint,首次有内容的渲染是当浏览器渲染 DOM 第一块内容,第一次回馈用户页面正在载入。
  3. TTI: Time to interactive 第一次可交互时间,此时用户可以真正的触发 DOM 元素的事件,和页面进行交互。
  4. FID: First Input Delay 第一输入延迟测量用户首次与您的站点交互时的时间(即,当他们单击链接,点击按钮或使用自定义的 JavaScript 驱动控件时)到浏览器实际能够的时间回应这种互动。
  5. TTFB: Time to First Byte 首字节时间,顾名思义,是指从客户端开始和服务端交互到服务端开始向客户端浏览器传输数据的时间(包括 DNS、socket 连接和请求响应时间),是能够反映服务端响应速度的重要指标。

如果你还不太熟悉这些指标也没关系,接下来的内容中,会结合实际用例分析这些指标。


渲染开销 The cost of rendering


客户端渲染 Client-side rendering


从服务端获取 HTML、CSS、JavaScript 都是需要成本的,以一个 CSR(客户端渲染)的网站为例,客户端渲染的网站依赖框架库(bundle)、应用程序(app)来进行初始化渲染,假设它有 1MB 的 JavaScript Bundle 代码,那么只有当这一大段的代码加载并执行完成以后,用户才能看到页面。

它的结构一般如下:

640 (2).png

分析一下它的流程:

  1. 用户输入网址进入网站,拉取 HTML 资源。

  1. HTML 资源中发现 script 标签加载的 bundle 再一次发起请求拉取 bundle。此时也是性能统计指标中的 FP 完成。

在这个阶段,页面基本上是没什么意义的,当然你也可以放置一些静态的骨架屏或者加载提示,来友好的提示用户。

  1. JavaScript bundle 下载并执行完毕,此时页面才真正渲染出有意义的内容。对应 FCP 完成。

当框架对 DOM 节点添加各类事件绑定后,用户才真正可以和页面交互,此时也对应 TTI 完成。

它的缺点在于,直到整个 JavaScript 依赖执行完成之前,用户都看不到什么有意义的内容。


服务端同构渲染 SSR with Hydration


基于以上客户端渲染的缺点以及用户对于 CSR 应用交互更加丰富的需求,于是诞生了集 SSR 和 CSR 的性能、SEO、数据获取的优点与一身的「同构渲染」,简单点说,就是:

  1. 第一次请求,在服务端就利用框架提供的服务端渲染能力,直接原地请求数据,生成包含完整内容的 html 页面,用户不需要等待框架的 js 加载就可以看到内容。
  2. 等到页面渲染后,再利用框架提供的 Hydration(注水)能力,让服务端返回的“干瘪”的 HTML 注册事件等等,变的丰富起来,拥有了各种事件后,就和传统 CSR 一样拥有了丰富多彩的客户端交互。

在同构应用中,只要 HTML 页面返回,用户就可以看到丰富多彩的页面:

而 JavaScript 加载完毕后,用户就可以和这些内容进行交互(比如点击放大、跳转页面等等……)


代码对比


典型的 CSR React 应用的代码是这样的:

640 (3).png

而 SSR 的代码则需要服务端的配合,

先由服务端通过 ReactDOMServer.renderToString 在服务端把组件给序列化成 html 字符串,返回给前端:

640 (4).png

前端通过 hydrate 注水,使得功能交互变的完整:

Vue 的 SSR 也是同理:


同构的缺陷


至此看来,难道同构应用就是完美的吗?当然不是,其实普通的同构应用只是提升了 FCP 也就是用户看到内容的速度,但是却还是要等到框架代码下载完成,hydrate 注水完毕等一系列过程执行完毕以后才能真正的可交互

并且对于 FID 也就是 First Input Delay 第一输入延迟这个指标来说,由于 SSR 快速渲染出内容,更容易让用户误以为页面已经是可交互状态,反而会使「用户第一次点击 - 浏览器响应事件」 这个时间变得更久。

因此,同构应用很可能变成一把「双刃剑」。

下面我们来讨论一些方案。


Pre-rendering 预渲染。


对于不经常发生变化的内容来说,使用预渲染是一种很好的办法,它在代码构建时就通过框架能力生成好静态的 HTML 页面,而不是像同构应用那样在用户请求页面时再生成,这让它可以几乎立刻返回页面。

当然它也有很大的限制:

  1. 1、只适用于静态页面。
  2. 2、需要提前列举出需要预渲染的 URLs。

流式渲染 Streaming


流式渲染可以让服务端对大块的内容分片发送,使得客户端不需要完整的接收到 HTML,而是接受到第一部分时就开始渲染,这大大提升了 TTFB 首字节时间。

在 React 中,可以通过 renderToNodeStream 来使用流式渲染:


渐进式注水 Progressive Hydration


我们知道 hydrate 的过程需要遍历整颗 React 节点树来添加事件,这在页面很大的情况下耗费的时间一定是很长的,我们能否先只对关键的部分,比如视图中可见的部分,进行「注水」,让这部分先一步可以进行交互?

想象一下它的特点:

  1. 组件级别的渐进式注水。
  2. 服务端依旧整页渲染。
  3. 页面可以根据优先级来分片“启动”组件。

通过一张动图来直观的感受一下普通注水(左)和渐进式注水(右)的区别:

可以看到用户第一次可以交互的时间大大的提前了。

光说不做假把式,我们看看用 React 完成这个功能的代码,首先我们需要准备一个组件 Hydrator 用来实现当某个组件进入视图范围以后再进行注水。

首先来看看应用的整体结构:

let load = () => import('./stream');
let Hydrator = ClientHydrator;
if (typeof window === 'undefined') {
  Hydrator = ServerHydrator;
  load = () => require('./stream');
}
export default function App() {
  return (
    <div id="app">
      <Header />
      <Intro />
      <Hydrator load={load} />
    </div>
  );
}

根据客户端和服务端的环境区分使用不同的 Hydrator,在服务端就直接返回普通的 html 文本:

function interopDefault(mod) {
  return (mod && mod.default) || mod;
}
export function ServerHydrator({ load, ...props }) {
  const Child = interopDefault(load());
  return (
    <section>
      <Child {...props} />
    </section>
  );
}

而客户端,则需要实现渐进式注水的关键部分:

export class Hydrator extends React.Component {
  render() {
    return (
      <section
        ref={c => (this.root = c)}
        dangerouslySetInnerHTML={{ __html: '' }}
        suppressHydrationWarning
      />
    );
  }
}

首先 render 部分,利用 dangerouslySetInnerHTML 来使得这部分初始化为空的 html 文本,并且由于 server 端肯定还是和往常一样全量渲染内容,而客户端由于初始化需要先不做任何处理,会导致 React 内部对于服务端内容和客户端内容的「一致性检测」失败。

而利用 dangerouslySetInnerHTML 的特性,会让 React 不再进一步 hydrate 遍历 children 而是直接沿用服务端渲染返回的 HTML,保证在注水前渲染的样式也是 OK 的。

再利用 suppressHydrationWarning 取消 React 对于内容一致性检测失败的警告。

export class Hydrator extends React.Component {
  componentDidMount() {
    new IntersectionObserver(async ([entry], obs) => {
      if (!entry.isIntersecting) return;
      obs.unobserve(this.root);
      const { load, ...props } = this.props;
      const Child = interopDefault(await load());
      ReactDOM.hydrate(<Child {...props} />, this.root);
    }).observe(this.root);
  }
  render() {
    return (
      <section
        ref={c => (this.root = c)}
        dangerouslySetInnerHTML={{ __html: '' }}
        suppressHydrationWarning
      />
    );
  }
}

接下来,组件在客户端初始化的时候,利用 IntersectionObserver 监控组件元素是否进入视图,一旦进入视图了,才会动态的去 import 组件,并且利用 ReactDOM.hydrate 来真正的进行注水。

此时不光注水是动态化的,包括组件代码的下载都会在组件进入视图时才发生,真正做到了「按需加载」。

640.gif

动图中紫色动画出现,就说明渐进式 hydrate 完成了。

对比一下全量注水和渐进式注水的性能会发现首次可交互的时间被大大提前了:

当然,我们了解原理就发现,不光可以通过监听组件进入视图来 hydrate,甚至可以通过 hoverclick 等时机来触发,根据业务需求的不同而灵活调整吧。

可以访问图片中的网址获取你喜欢的框架在这方面的相关文章:


总结


本文通过总结了 Rendering on the Web: Performance Implications of Application Architecture (Google I/O ’19)[2] 这段 Google 团队的精彩演讲,来介绍了现代应用架构体系中的优化手段,包括:

  • 预渲染
  • 同构渲染
  • 流式渲染
  • 渐进式注水

在不同的业务场景下选择对应的优化手段,是一名优秀的前端工程师必备的技能,相信看完这篇文章的你一定有所收获。

完整 demo 地址:

https://github.com/GoogleChromeLabs/progressive-rendering-frameworks-samples

参考资料

[1]

Rendering on the Web: Performance Implications of Application Architecture (Google I/O ’19): https://www.youtube.com/watch?v=k-A2VfuUROg&feature=youtu.be&t=1036

[2]

Rendering on the Web: Performance Implications of Application Architecture (Google I/O ’19): https://www.youtube.com/watch?v=k-A2VfuUROg&feature=youtu.be&t=1036


相关文章
|
2月前
|
人工智能 自然语言处理 开发工具
统一多模态 Transformer 架构在跨模态表示学习中的应用与优化
本文介绍统一多模态 Transformer(UMT)在跨模态表示学习中的应用与优化,涵盖模型架构、实现细节与实验效果,探讨其在图文检索、图像生成等任务中的卓越性能。
统一多模态 Transformer 架构在跨模态表示学习中的应用与优化
|
3月前
|
存储 编解码 Serverless
Serverless架构下的OSS应用:函数计算FC自动处理图片/视频转码(演示水印添加+缩略图生成流水线)
本文介绍基于阿里云函数计算(FC)和对象存储(OSS)构建Serverless媒体处理流水线,解决传统方案资源利用率低、运维复杂、成本高等问题。通过事件驱动机制实现图片水印添加、多规格缩略图生成及视频转码优化,支持毫秒级弹性伸缩与精确计费,提升处理效率并降低成本,适用于高并发媒体处理场景。
160 0
|
4月前
|
人工智能 监控 安全
NTP网络子钟的技术架构与行业应用解析
在数字化与智能化时代,时间同步精度至关重要。西安同步电子科技有限公司专注时间频率领域,以“同步天下”品牌提供可靠解决方案。其明星产品SYN6109型NTP网络子钟基于网络时间协议,实现高精度时间同步,广泛应用于考场、医院、智慧场景等领域。公司坚持技术创新,产品通过权威认证,未来将结合5G、物联网等技术推动行业进步,引领精准时间管理新时代。
|
4月前
|
前端开发 算法 API
构建高性能图像处理Web应用:Next.js与TailwindCSS实践
本文分享了构建在线图像黑白转换工具的技术实践,涵盖技术栈选择、架构设计与性能优化。项目采用Next.js提供优秀的SSR性能和SEO支持,TailwindCSS加速UI开发,WebAssembly实现高性能图像处理算法。通过渐进式处理、WebWorker隔离及内存管理等策略,解决大图像处理性能瓶颈,并确保跨浏览器兼容性和移动设备优化。实际应用案例展示了其即时处理、高质量输出和客户端隐私保护等特点。未来计划引入WebGPU加速、AI增强等功能,进一步提升用户体验。此技术栈为Web图像处理应用提供了高效可行的解决方案。
|
3月前
|
消息中间件 存储 Kafka
一文带你从入门到实战全面掌握RocketMQ核心概念、架构部署、实践应用和高级特性
本文详细介绍了分布式消息中间件RocketMQ的核心概念、部署方式及使用方法。RocketMQ由阿里研发并开源,具有高性能、高可靠性和分布式特性,广泛应用于金融、互联网等领域。文章从环境搭建到消息类型的实战(普通消息、延迟消息、顺序消息和事务消息)进行了全面解析,并对比了三种消费者类型(PushConsumer、SimpleConsumer和PullConsumer)的特点与适用场景。最后总结了使用RocketMQ时的关键注意事项,如Topic和Tag的设计、监控告警的重要性以及性能与可靠性的平衡。通过学习本文,读者可掌握RocketMQ的使用精髓并灵活应用于实际项目中。
1609 7
 一文带你从入门到实战全面掌握RocketMQ核心概念、架构部署、实践应用和高级特性
|
2月前
|
人工智能 数据可视化 Java
什么是低代码(Low-Code)?低代码核心架构技术解析与应用展望
低代码开发正成为企业应对业务增长与IT人才短缺的重要解决方案。相比传统开发方式效率提升60%,预计2026年市场规模达580亿美元。它通过可视化界面与少量代码,让非专业开发者也能快速构建应用,推动企业数字化转型。随着AI技术发展,低代码与AIGC结合,正迈向智能化开发新时代。
|
4月前
|
缓存 前端开发 应用服务中间件
Web端实时通信技术SSE在携程机票业务中的实践应用
本文介绍了携程机票前端基于Server-Sent Events(SSE)实现服务端推送的企业级全链路通用技术解决方案。文章深入探讨了 SSE 技术在应用过程中包括方案对比、技术选型、链路层优化以及实际效果等多维度的技术细节,为类似使用场景提供普适性参考和借鉴。
107 7
|
4月前
|
机器学习/深度学习 算法 测试技术
图神经网络在信息检索重排序中的应用:原理、架构与Python代码解析
本文探讨了基于图的重排序方法在信息检索领域的应用与前景。传统两阶段检索架构中,初始检索速度快但结果可能含噪声,重排序阶段通过强大语言模型提升精度,但仍面临复杂需求挑战
121 0
图神经网络在信息检索重排序中的应用:原理、架构与Python代码解析
|
4月前
|
安全 Devops 测试技术
AppSpider 7.5.018 for Windows - Web 应用程序安全测试
AppSpider 7.5.018 for Windows - Web 应用程序安全测试
73 0
AppSpider 7.5.018 for Windows - Web 应用程序安全测试

热门文章

最新文章