Web 性能优化:CSS层面提高FPS的方法
Author:zhoulujun Date:
打王者,我们一般会开启高帧率模式,哪怕降低画质,就是位了提升游戏的流畅的——掉帧的情况下,根本无法玩!
大多数设备的刷新率都是 60 FPS,如果浏览器在交互的过程中能够时刻保持在 60FPS 左右,用户就不会感到卡顿,否则,就会影响用户的体验。
为了达到理想的 60 FPS(即每帧大约 16.67 毫秒)
浏览器运行的单个帧的渲染流水线,称为像素管道,如果其中的一个或多个环节执行时间过长就会导致卡顿。像素管道是作为开发者能够掌握的对帧性能有影响的部分,其他部分由浏览器掌握,我们无法控制。我们的目标就是就是尽快完成这些环节,以达到 60 FPS 的目标。
JavaScript:通常来说,阻塞的发起都是来自于 JS ,这不是说不用 JS,而是要正确的使用 JS 。首先,JS 线程的运行本身就是阻塞 UI 线程的(暂不考虑 Web Worker)。从纯粹的数学角度而言,每帧的预算约为 16.7 毫秒(1000 毫秒 / 60 帧 = 16.66 毫秒/帧)。但因为浏览器需要花费时间将新帧绘制到屏幕上,只有 ~10 毫秒来执行 JS 代码,过长时间的同步执行 JS 代码肯定会导致超过 10ms 这个阈值,其次,频繁执行一些代码也会过长的占用每帧渲染的时间。此外,用 JS 去获取一些样式还会导致强制同步布局(后面会有介绍)。
样式计算(Style):此过程是根据匹配选择器(例如 .headline 或 .nav > .nav__item)计算出哪些元素应用哪些 CSS 规则的过程,这个过程不仅包括计算层叠样式表中的权重来确定样式,也包括内联的样式,来计算每个元素的最终样式。
布局(Layout):在知道对一个元素应用哪些规则之后,浏览器即可开始计算该元素要占据的空间大小及其在屏幕的位置。网页的布局模式意味着一个元素可能影响其他元素,一般来说如果修改了某个元素的大小或者位置,则需要检查其他所有元素并重排(re-flow)整个页面。
绘制(Paint):绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个表面(通常称为层)上完成的,绘制包括两个步骤: 1) 创建绘图调用的列表, 2) 填充像素,后者也被称作栅格化。
合成(Composite):由于页面的各部分可能被绘制到多个层上,因此它们需要按正确顺序绘制到屏幕上,才能正确地渲染页面。尤其对于与另一元素重叠的元素来说,这点特别重要,因为一个错误可能使一个元素错误地出现在另一个元素的上层。
上节渲染管道的每个环节都有可能引起卡顿,所以要尽可能减少通过的管道步骤。
如果SVG层面的,会划分的更细
CPU将大量的层数据提交给GPU影响commit时间的主要是层的数量和大小。这些层产生的原因是由于svg元素的动画大量使用的filter导致的。同样是由于filter导致大量的层不能做压缩处理。、
采用更好的 CSS 方法进行优化
修改不同的样式属性会有以下几种不同的帧流程,在这里就直接贴 Google Developers 的图了:
减少重绘/重排
我们可以看到 JS,Style 和 Composite 是不可避免的,因为需要 JS 来引发样式的改变,Style 来计算更改后最终的样式,Composite 来合成各个层最终进行显示。Layout 和 Paint 这两个步骤不一定会被触发,所以在优化的过程中,如果是需要频繁触发的改变,我们应该尽可能避免 Layout 和 Paint。
想知道每种 CSS 属性的更改是否会触发 Layout,Paint,Composite,可以通过https://csstriggers.com/查看。
首先是减少重绘重排,就是尽可能减少重绘的影响面与工作量!
尽量使用合成器单独处理的属性
性能最佳的像素管道版本会避免 Layout 和 Paint,为了提高渲染性能和效率,重要的是要确保尽量让浏览器能够使用其合成器(Compositing Layer)来单独处理某些元素的渲染。这通常涉及到确保元素(或其父元素)具有独立的图层(layer),这样浏览器就可以独立于其他DOM元素来渲染它们,进而利用GPU加速来提升性能。
如何提升元素到新的层
有一种能有效减小 Layout 和 Paint 的方法是将元素提升,像 Photoshop 中层的概念一样,样式也有层的概念,不同的层根据不同顺序叠加起来,通过 Composite 最终显示出来。在每个层中对这个层进行 Layout 或者 Paint 是不会影响其他层的,一般会根据整个页面的语义将页面分为几个层。
使用will-change属性:
will-change属性告诉浏览器某个元素的属性在不久的将来可能会发生变化,因此浏览器可以预先为这个元素创建一个独立的图层。但请谨慎使用,因为滥用会导致内存使用过多和页面性能下降。
使用position: absolute或fixed:
当元素使用position: absolute;或position: fixed;时,这些元素会脱离文档流,这有助于浏览器将它们放在新的图层上。但请注意,这并不总是自动导致新图层的创建,但它是促进独立渲染的一个好方法。
contain布局计算限制
拥有 contain 属性(不为 none)的元素与页面其他元素相对独立,并且该元素及其后代元素样式、DOM 发生变化时不会导致整个页面回流和重绘。
具体参看《css布局优化:布局计算限制— contain/will-change/合成层》
触发硬件加速的CSS属性:
CSS3 硬件加速又叫做 GPU 加速,是利用 GPU 进行渲染,减少 CPU 操作的一种优化方案。
使用能够触发硬件加速的CSS属性,如transform、opacity、filters等。这些属性通常会强制浏览器创建新的图层,因为它们需要更复杂的渲染技术,而GPU更擅长处理这类计算。
同时由于 GPU 中的 transform 等 CSS 属性不会触发 repaint,所以能大大提高网页的性能。
不要滥用层
但是不要滥用层,将每个元素都单独提升到一层, Composite 这个环节有两步,Update Layer Tree 和 Composite Layer Tree,前者负责计算页面中有多少个层,哪些层应该出现并应该按什么顺序叠加起来,后者负责将 layers 合成到屏幕上。层越多,这两个步骤花的时间越长,同时也会占用更多的内存,所以要在适当的地方提升元素而不是对所有元素都进行提升。
提升元素还有一个好处就是会将动画从 CPU 转移到 GPU 来完成,来实现硬件加速。
减小选择器匹配的难度
通过上面的几种不同流程的管道图可以发现,只要是修改样式那么必不可少会经过 Style,计算样式的第一步是创建一组匹配选择器,这实质上是浏览器计算出给指定元素应用哪些类、伪选择器和 ID 。第二步是从对应的匹配选择器中获取所有样式规则,并计算出此元素的最终样式,简单的来说就是第一步先确定选择器都匹配哪些元素,第二步根据每个元素所匹配的选择器,通过权重计算出最终的样式。
对于要匹配相同的元素,.final-box-title 比 .box:nth-last-child(-n+1) .title 明显复杂度要来的小得多,浏览器不需要去判断要查找的元素是不是最后一个元素即可根据类名快速找到 .final-box-title 对应的元素,相比复杂的选择器,简单地将选择器与元素匹配开销要小得多,而且嵌套过深的 CSS 选择器依赖了过多的类名,很容易在改动依赖的类名时不小心被影响到。
这里推荐使用 BEM(块、元素、修饰符) 编码规则简化选择器规则,该方法实际上纳入了上述选择器匹配的性能优势,因为它建议所有元素都有单个类,并且在需要层次结构时也纳入了类的名称。
避免强制同步布局
什么是强制同步重排 - FSL (forced synchronous layout)
浏览器的工作原理:新式网络浏览器幕后揭秘 将布局分为异步布局和同步布局:
增量布局是异步执行的。Firefox 将增量布局的“reflow 命令”加入队列,而调度程序会触发这些命令的批量执行。WebKit 也有用于执行增量布局的计时器:对呈现树进行遍历,并对 dirty 呈现器进行布局。
请求样式信息(例如“offsetHeight”)的脚本可同步触发增量布局。
全局布局往往是同步触发的。有时,当初始布局完成之后,如果一些属性(如滚动位置)发生变化,布局就会作为回调而触发。
除了影响所有呈现器的全局样式更改,例如字体大小更改和屏幕大小调整的更改都是增量修改,增量修改是异步的也就给了我们用 thunk 修改的机会。
如果我们在 js 中这样写
let boxes = document.getElmentsByClassName('.box') for(let i = 0; i < boxes.length; i++) { let width = document.getElementById('table') boxes[i].style.color = 'red' }
这种情况下,这一帧相比上一帧没有布局没有发生改变,那么直接用旧的 Layout 去赋值 width 就可以,也不需要对页面进行重排。
但是如果这样写:
let boxes = document.getElmentsByClassName('.box') for(let i = 0; i < boxes.length; i++) { let width = document.getElementById('table').width boxes[i].style.width = width }
当下一次循环到来时浏览器还没进重排(因为一直处于 JS 阶段) ,为了获取正确的 width ,浏览器就不得不立刻重新 Layout 获取一个最新值,从而失去了浏览器自身的批量更新的优化,这就是强制同步布局。
为什么叫强制呢,大多数浏览器通过队列化修改并批量执行来优化重排过程(就是上面说的异步布局),但是如果触发了强制同步布局 ,每经过一次循环,都会要求浏览器强制刷新队列并要求计划任务立刻执行,这就失去了浏览器对重排的优化。
什么操作会触发强制同步布局 呢,这个 gist 里列出了对应的操作。
如何避免强制同步布局
使用 requestAnimationFrame
缓存不动变量,对上面的那个强制同步布局的例子,避免在循环中进行可能会导致强制同步布局的操作
FLIP策略
介绍一下 Paul Lewis 发明的 FLIP 方法,FLIP 就是 F (first) L (last) I (invert) P (play) 的缩写。
FLIP 将一些性能低下的动画映射为 transform 动画。通过记录元素的两个快照,一个是元素的初始位置(First – F),另一个是元素的最终位置(Last – L),然后对元素使用一个 transform 变换来反转(Invert – I),让元素看起来还在初始位置,最后移除元素上的 transform 使元素由初始位置运动(Play – P)到最终位置。
它就是通过这样一种高性能的方式来动态的改变DOM元素的位置和尺寸,而不需要管它的布局是如何计算或渲染的(比如,height、width、float、绝对定位、Flexbox和Grid等)。
上面这个过程可以拆解为以下四个步骤:
first: 在整个动画过程中元素的起始状态,对应动画的Start阶段,
Last: 在整个动画过程中元素的终止状态,对应动画的End阶段。
Invert: 在元素处于End位置,利用 transform 做一个逆运算,让添加了 transform 的元素回归到初始位置。可以使用Flip Plugin做!
这一步是关键,通过 First 和 Last 计算出来的状态,得到一个从 Last 到 First 的变化倍率(比如大小或位置,是的,是从 Last 到 First),然后让元素具有终止状态的 class 及刚刚计算出来的 invert state 的 transform 属性,他们两个相抵消,元素在视觉上还是没有任何变化。举个例子,比如我们想让一个元素向右移动 10px,再放大两倍,那么这个计算出来的相反的 transfrom 属性就应该是 transform: translateX(-10px) scale(0.5),再给他一个 left: 10px; width: 200px; height: 200px;(假设原来是 left: 0; width: 100px; height: 100px;),这两个属性视觉效果上抵消,好像元素从来没有改变过。
Play: 真正需要执行动画时,给元素添加一个 transition 效果,再移除元素的 transform 属性,因为此时元素已经是终止状态了,所以就会 transition 到 0,整个过程只有 transform ,可以轻松达到 60FPS。
核心思想就是 pre-calculation
使用FLIP形式需要注意什么
FLIP中的前三个阶段也就是前期的准备工作需要在绘制这个步骤之前,也就是时间需要尽可能的控制在之前提到的100ms以内,否则的话 渲染的过程中可能出现闪烁,但是我们怎么才能抓住绘制前这个时机呢?本人用React比较多,以React为例:答案是useLayoutEffect,它接受一个回调函数,这个函数会在dom更新后、重绘之前同步的执行
参考文章:
前端性能优化之浏览器渲染优化 —— 打造60FPS页面 https://github.com/fi3ework/Blog/issues/9
拯救动画卡顿之FLIP https://juejin.cn/post/7181744967000752165
FLIP技术让动画更流畅 https://juejin.cn/post/6881903754610737159
转载本站文章《Web 性能优化:CSS层面提高FPS的方法》,
请注明出处:https://www.zhoulujun.cn/html/webfront/style/css3/2024_0807_9211.html