webVueBlog / WebGuideInterview

「WebGuideInterview学习」每日 3+1,以面试题来驱动学习,提倡每日学习与思考,每天进步一点!每天早上纯手工发布(死磕自己,愉悦大家)准备 前端 面试,首选 WebGuideInterview!面试题大收集,面试集锦 ❤ 💝 💘

Home Page:https://webvueblog.github.io/WebGuideInterview/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

渲染性能优化全局视角

webVueBlog opened this issue · comments

硬件视角

广义上看性能的本质是:在体验、处理能力和功耗三个方向上找到平衡点。我这样定义的灵感是来自于硬件芯片设计,芯片设计从硬件工程视角对芯片要求:面积、性能、功耗三个方向上找到平衡点。在使用 FPGA、CPLD 进行芯片设计和验证的时候,逻辑门数量受到芯片生产工艺和面积的限制而产生一个总体的面积约束,这时候,要使用一些逻辑门组合成专用电路(所谓 IP)从而提升性能并降低功耗(专用电路功耗小于软件加通用电路),否则,只能设计成通用电路如:寄存器操作、通用处理指令……等。因此,可以看出,在条件允许的情况下,专用电路也就是所谓“专业化”能够提供更好的的面积、性能、功耗比。

Apple 从软件生态到操作系统、从底层系统 API 到硬件驱动、从驱动到硬件电子电路设计时全局规划的,这种全局规划保证了软件调用最频繁的系统程序硬件化,在提高性能功耗比的同时也保证了专业化电路的使用率,这就是我所谓的:硬件视角。

从 Android 的开源生态系统里找到蛛丝马迹,把这些开源生态系统的能力按照:软件生态到操作系统内、从底层系统 API 到硬件驱动、从驱动到硬件提供的专业电子电路的路径梳理出来,在根据这个路径对应到软件工程上,从硬件视角全局性的看待性能优化问题,充分利用底层硬件的能力。

前端包含了渲染和计算两个部分。渲染部分由 HTML 和 CSS 共同定义,之后,交由浏览器进行渲染,因此,浏览器确实屏蔽了大部分连接到底层能力的部分,造成前端缺乏抓手。但是,随着 WebGL、WebGPU 等全新 API 暴露,前端还是有一些抓手的。计算部分由 JavaScript 定义,之后,交由脚本引擎执行,因此,脚本引擎屏蔽了大部分连接到底层能力的部分,造成前端缺乏抓手,同时,脚本引擎基本都用虚拟机屏蔽底层指令集和硬件差异,因此,又多了一层屏蔽。但是,随着 Node.js 和 WASM 等技术让部分程序 Local 化本地执行,随着 V8 引擎用一些特殊策略让部分 JavaScript 被 Sparkplug 编译成 Local 代码执行,这种情况也会有所改变。

前端已经在很多场景中穿透 浏览器/webview 更直接的和底层硬件打交道,硬件视角将成为:在体验、处理能力和功耗三个方向上找到平衡点的关键,接下来,将针对渲染、计算这两个渲染性能优化的核心场景分别进行介绍。

渲染视角

其实,在上文中 HTML 和 CSS 定义了渲染的说法过于粗糙,精确的表述应该是:HTML 和 CSS 定义了渲染的内容,而 JavaScript 可以干预内容和渲染过程。没有 JavaScript 的干预,HTML 和 CSS 定义的渲染就是静态 HTML 页面的渲染
,在 DHTML 和 XSLT 销声匿迹后,动态渲染更多是由 JavaScript 完成的,这体现了解耦后各司其职的优势,同时,把动态渲染能力从简单的 API 调用提升到编程语言的复杂逻辑控制,释放无限的可能性。综上所述,我把渲染视角拆分为:渲染内容、渲染过程、JavaScript 干预三个部分,分别阐释一下我的理解。

渲染内容

首先,WWW 的出现将人类从信息孤岛推向了互联互通的万维网时代,而信息的载体就是 Web 上的 HTML 俗称:超文本标记语言,彼时,将 HTML 渲染出来的核心是排版引擎,Web 标准也是围绕着排版展开的。随着技术发展,人们对于排版出来的静态内容逐渐疲劳,DHTML 和 XHTML(XML + XSLT)等技术和高级的 API 如:HTTPWebRequest ……带来了动态能力,Flash、Silverlight、JavaAplate ……等技术带来了富客户端能力,随着 Web 2.0 去中心化打破门户的垄断,整个互联网产业带来了空前的繁荣。

随着行业的发展和技术的进步,渲染内容从最初的“文档排版”承载简单信息,到“富媒体”承载多媒体信息,再到今天的增强现实技术 WebXR 承载复杂的数字和现实混合信息,渲染内容对 渲染引擎、显示能力、硬件加速能力都提出了不同的要求。最简单的说,任何一个引擎都会把 Animation 相关 API 单独拎出来,从而区分这种高负载的渲染工作,在框架和底层引擎上进行特殊优化。

此外,在显示能力上渲染内容的不同也会产生差异,最常见是分辨率、HDR……等内容对显示能力有特殊的要求。硬件加速则更容易理解,针对不同负载的渲染工作,首先降低 CPU 、内存、磁盘 I/O 的压力,其次是更多用 GPU 、DSP 等专业电子电路进行替代,从而达到更高的性能/功耗比。

了解这些是为了区分软件工程里构建内容,这些内容的选择对渲染性能有决定性的影响,这种差异又受限于底层的硬件如:CPU、GPU、DSP、专业加速电路……等,因此,他们是从内容解析开始,到底层硬件加速能力,浑然一体且密切相关的。即便在浏览器引擎的屏蔽下缺少直接控制的能力,对于选择何种内容的表达方式(端应用里 UI 控件和 Draw API 选择、Web 里 HTML 标记和 CSS 样式选择对 Paint 的影响……等)都需要有穿透到底层的视角和理解力,才能在“要你命 3000 ”的基础上描绘一条更接近问题本质的优化路径。

具备了渲染内容的把控力,实现上就不必赘述了,很简单的一远一近两步即可。所谓“一远”是指根据业务需求和产品设计把视角穿透到底层和硬件审查技术能力可以带来什么新的、有意思的东西。所谓“一近”就是收回视角选择合适的 UI 控件和 Draw API、HTML 标记和 CSS 去构建要渲染的内容,剩下的就是中间的渲染过程。

渲染过程

从成像原理上说,渲染过程包括:CPU 计算(UI 引擎或浏览器引擎、渲染引擎的工作)、图形渲染 API (OpenGL/Metal)、GPU 驱动、GPU渲染、VSYNC 信号发射,HSYNC 信号发射的过程。常见的渲染问题有:卡顿、撕裂、掉帧……,卡顿、撕裂、掉帧通常都是渲染时间过长造成,渲染时间问题大多数情况下是耗费在 CPU 计算上,部分情况下是耗费在图形渲染上,怎么理解呢?其实很简单,把一个渲染性能很差的复杂页面用高端机流畅渲染过程录屏,再拿个低端机播放录屏的视频并在同一个手机上打开页面让浏览器进行渲染,图像复杂度没有差异渲染性能上视频播放却比页面渲染快很多,这就是在展示 CPU、GPU 计算和图形渲染的耗时,因为视频的解码和渲染相较浏览器引擎更加简单且渲染过程更短。(特殊编解码格式和高码率的视频除外)

因此,从渲染过程来看,性能优化的本质在首先于降低 CPU、GPU 计算负载。其次,如果有条件(实现差异对业务有影响需要说服业务方)通过不同的渲染内容构建方法去影响渲染过程的话,优先选择有 CPU、GPU 优化指令和专用电子电路加速的底层 API 来构建渲染内容。例如在 H.264 硬件加速普及的今天,是否应该用 X.265 / H.265 就值得商榷。

在探讨渲染过程的时候,流畅性指标是首先需要关注的,根据 60Hz 刷新率下 16.6ms 的帧渲染速度,可以从时间角度定义 16.6ms x 2 (双缓冲)、16.6ms x 3(三缓冲)的 CPU 和 GPU 处理时间,压缩渲染过程来保证流畅度。

OOPD (Out of Process Display Compositor,进程外 Display 合成器),它的主要目的是将 Display Compositor 从 Browser 进程迁移到 Viz 进程(也就是原来的 GPU 进程),Browser 则变成了 Viz 的一个 Client,Renderer 跟 Viz 建立链接(CompositorFrameSink)虽然还是要通过 Browser,但是建立链接后提交 CompositorFrame 就是直接提交给 Viz 了。Browser 同样也是提交 CompositorFrame 给 Viz,最后在 Viz 生成最终的 CompositorFrame,通过 Display 交由 Renderer 合成输出。

OOPR (Out of Process Rasterization,进程外光栅化)OOPR 跟目前的 GPU 光栅化机制的主要区别是:

在当前的 GPU 光栅化机制中,Worker 线程在执行光栅化任务时,会调用 Skia 将 2D 绘图指令转换成 GPU 指令,Skia 发出的 GPU 指令通过 Command Buffer 传送到 Viz 进程的 GPU 线程中执行;

在 OOPR 中,Worker 线程在执行光栅化任务时,它直接将 2D 绘图指令(DisplayItemList)序列化到 Command Buffer 传送到 GPU 线程,运行在 GPU 线程的部分再调用 Skia 去生成对应的 GPU 指令,并交由 GPU 直接执行;

简而言之,就是将 Skia 光栅化的部分从 Renderer 进程转移到了 Viz 进程。当 OOPD,OOPR 和 SkiaRenderer 都开启后:

光栅化和合成都迁到了 Viz 进程;

光栅化和合成都使用 Skia 做 2D 绘制,实际上 Chromium 所有的 2D 绘制最终都交由 Skia 来做,由 Skia 生成对应的 GPU 指令;

光栅化和合成时,Skia 最终输出 GPU 指令都在 GPU 线程,并且使用同一个 Skia GrContext(Skia 内部的 GPU 绘图上下文);

  1. 光栅化和合成都迁到了 Viz 进程;
  2. 光栅化和合成都使用 Skia 做 2D 绘制,实际上 Chromium 所有的 2D 绘制最终都交由 Skia 来做,由 Skia 生成对应的 GPU 指令;
  3. 光栅化和合成时,Skia 最终输出 GPU 指令都在 GPU 线程,并且使用同一个 Skia GrContext(Skia 内部的 GPU 绘图上下文);

一句话概括:JavaScript 尽快执行和返回。下文将从 Passer 、Layout 、Compositor 的计算视角去分析:计算复杂度对渲染性能的影响。

CRP 概览

通过网络 I/O 或磁盘 I/O (缓存)加载 HTML CSS 之后的链路:解码 HTML、CSS 文件(GZip 文本传输前压缩等)、处理过程(HTML、CSS Passing)、DOM Tree 构建、Style 内联、布局、合成器、绘制,这里涉及浏览器引擎进行大量的解析和计算等处理过程,为此,需要引入一个概念:关键渲染路径(Critical Rendering Path),简称:CRP

首先,一旦浏览器得到响应,它就开始解析它。当它遇到一个依赖关系时,它就会尝试下载它

如果它是一个样式文件(CSS文件),浏览器就必须在渲染页面之前完全解析它(这就是为什么说CSS具有渲染阻碍性)

如果它是一个脚本文件(JavaScript文件),浏览器必须: 停止解析,下载脚本,并运行它。只有在这之后,它才能继续解析,因为 JavaScript 脚本可以改变页面内容(特别是HTML)。(这就是为什么说JavaScript阻塞解析)

一旦所有的解析工作完成,浏览器就建立了 DOM 树和CSSOM树。将它们结合在一起就得到了渲染树。

倒数第二步是将渲染树转换为布局。这个阶段也被称为 重排

最后一步是绘制。它涉及到根据浏览器在前几个阶段计算出来的数据对像素进行字面上的着色

把这几步放到渲染引擎渲染页面的过程中来,就能更清晰的认识到,CRP 会经过下面几个过程:

image

简单地说,CRP 的步骤:

  • 处理 HTML 标记并构建 DOM 树
  • 处理 CSS 标记并构建 CSSOM 树
  • 将 DOM 树和 CSSOM 树合并大一个渲染树
  • 根据渲染树来布局
  • 将各个节点绘制到屏幕上

注意:当 DOM 或者 CSSOM 发生变化的时候(JavaScript可以通过 DOM API 和 CSSOM API 对它们进行操作,从而改变页面视觉效果或内容)浏览器就需要再次执行上面的步骤,上文介绍的 Virtual-Tree 渲染优化就源自这里

优化页面的关键渲染路径(Critical Rendering Path)三件事:

  • 减少关键资源请求数: 减小使用阻塞的资源(CSS 和 JS),注意,并非所有资源是关键资源,尤其是 CSS 和 JS(比如使用媒体查询的 CSS,使用异步的 JS 就不关键了)
  • 减少关键资源大小:使用各种手段,比如减少、压缩和缓存关键资源,数据量越小,引擎计算复杂度越小
  • 缩短关键渲染路径长度

在具体优化 CRP 时可以按下面的常规步骤进行:

  • 对 CRP 进行分析和特性描述,记录 关键资源数量、关键资源大小 和 CRP 长度
  • 最大限度减少关键资源的数量:删除它们,延迟它们的下载,将它们标记为异步等
  • 优化关键资源字节数以缩短下载时间(往返次数),减少 CRP 长度
  • 优化其余关键资源的加载顺序,需要尽早下载所有关键资源,以缩短 CRP 长度

使用 Lighthouse 或 Navigatio Timing API 检测关键请求链

我们需要一些工具帮助我们检测 CRP 中一些重要指标,如:关键资源数量、关键资源大小 、CRP 长度等。在Chrome中使用 Lighthouse插件,Node 版本的 Lighthouse:

// 安装lighthouse
» npm i -g lighthouse 

» lighthouse https://jhs.m.taobao.com/ --locale=zh-CN --preset=desktop --disable-network-throttling=true --disable-storage-reset=true --view

更详细的可以查阅 Lighthouse 官网文档

除了使用 Lightouse 检测工具之外,还可以使用 Navigation Timing API 来捕获并记录任何页面的真实 CRP 性能。

image

我们也可以使用性能监控规范中相关 API 来对页面真实用户场景的性能监控:

image

通过相应工具或技术手段获得 CRP 分析结果之后,就可以针对性的进行优化。

CRP 优化策略

计算视角下的 HTML

编写有效的可读的 DOM:

  • 用小写字母书写,每个标签都应该是小写的,所以请不要在HTML标签中使用任何大写字母
  • 关闭自我封闭的标签
  • 避免过渡使用注释(建议使用相应工具清除注释)
  • 组织好DOM,尽量只创建绝对必要的元素

减少 DOM 元素的数量(在 Blink 内核的 HTMLDocumentParser 处理中 Token 的个数和 DOM 元素数量息息相关,减少 Token 数量可以加快 DOM Tree 的构建,从而加快排版渲染首帧的速度),因为页面上有过多的 DOM 节点会减慢最初的页面加载时间、减慢渲染性能,也可能导致大量的内存使用。因此请监控页面上存在的DOM元素的数量,确保你的页面没有:

  • 没有超过 1500 个 DOM 节点
  • DOM节点的嵌套没有超过 32级
  • 父节点没有 60 个以上的子节点

计算视角下的 CSS

CSS类(class)的长度:类名的长度会对 HTML 和 CSS 文件产生轻微的影响 (在某些场景下存有争议,详细参阅 CSS 选择器性能)

关键CSS(Critical):将页面 CSS 分为关键 CSS (Critical CSS)和 非关键CSS(No-Critical CSS),关键CSS 通过 <style> 方式内联到页面中(尽可能压缩后引用),可以使用 critical 工具来完成(详细参阅 关键CSS)

使用媒体查询:符合媒体查询的样式才会阻塞页面的渲染,当然所有样式的下载不会被阻塞,只是优先级会调低。

避免使用 @import 引入CSS:被 @import 引入的 CSS 需要依赖包含的 CSS 被下载与解析完毕才能被发现,增加了关键路径中的往返次数,意味着浏览器引擎的计算负载加重。

分析样式表的复杂性:分析样式表有助于发现有问题的,冗余 和重复的 CSS 选择器。分析出 CSS 中冗余和重复 CSS选择器,可以删除这些代码,加速 CSS 文件读取和加载。可以使用 TestMyCSSanalyze-cssProject WallaceCSS Stats来帮助你分析和更正CSS代码

不同的 HTML 标记和 CSS 样式选择,以及我们在其中使用的布局方法,会在不经意间,对布局引擎产生计算负载。避免这种负载的产生,就是让布局引擎尽可能少的计算,可以用前文所述空间换时间的方法,用确定性的数值去避免布局引擎的估计或计算。