关于每秒60帧

关于每秒60帧

看了一些介绍FPS和浏览器刷新率的科普文章,直接抛结论:

目前大部分电脑的屏幕刷新率是60Hz即每秒钟页面会刷新60次,也即是60帧的画面。这是显示器硬件层面的,浏览器的刷新频率一般与屏幕的刷新率保持一致或者略低于屏幕的刷新率,基本也是60Hz,所以平均每一帧的耗时是1000/60 = 16.67ms. (浏览器的fps高于显示器的fps是无意义的性能浪费,因为即使你绘制的贼快,屏幕不刷新依旧看不出来变化~)

不同帧率给人的直观感受:

  • 帧率能够达到 50 ~ 60 FPS 的动画将会相当流畅,让人倍感舒适;
  • 帧率在 30 ~ 50 FPS 之间的动画,因各人敏感程度不同,舒适度因人而异;
  • 帧率在 30 FPS 以下的动画,让人感觉到明显的卡顿和不适感;
  • 帧率波动很大的动画,亦会使人感觉到卡顿

要想页面给用户丝滑顺畅的感受,就要努力完成每秒60帧的目标💪,具体到每一帧就要控制在16.67ms之内,因为浏览器内部通常还会有不同的线程之间调度的工作,所以通常留给我们的V8的时间只有10ms,我们应该尽量保证在10ms的时间内完成js的执行。。。

用像素管道的方式来看前端优化问题

像素管道

浏览器将像素绘制到页面一般要经过以下几个过程:

JS / CSS -> style -> layout -> paint -> composite

1、 js: 一般我们使用js来实现一个视觉变化的效果。

2、 style: 样式计算,根据匹配选择器计算出哪些元素应用哪些css规则的过程

3、 layout: 布局,在知道对一个元素应用哪些规则之后,浏览器即可开始计算它要占据的空间大小及其在屏幕的位置

4、 paint: 绘制,有时候也叫栅格化是一个fill像素点的过程

5、 composite: 合成,用于处理不同的层级之间的关系,比如谁盖在谁的上面

一个帧内要做这么多事情。。。。如果js执行时间过长超过16.67ms,就会block住,那么就会丢掉一帧的绘制。

那么如何着手优化它呢?

JS,Style 和 Composite 是不可避免的,因为需要 JS 来引发样式的改变,Style 来计算更改后最终的样式,Composite 来合成各个层最终进行显示。Layout 和 Paint 这两个步骤不一定会被触发,所以在优化的过程中,如果是需要频繁触发的改变,我们应该尽可能避免 Layout 和 Paint。

1、最耗费性能之重排!

管道运行方式
JS/CSS —> Style —> Layout —> Paint —> Composite


此过程就是我们常说的浏览器重排,也就是改变了元素的几何属性(例如宽度、高度、左侧或顶部位置等),那么浏览器将必须检查所有其他元素,然后“自动重排”页面。任何受影响的部分都需要重新绘制,而且最终绘制的元素需进行合成,重排进行了管道的每一步,性能受到较大影响。

2、稍好之重绘
管道运行方式
JS/CSS —> Style —> Paint —> Composite


重绘,也就是修改“paint only”属性(例如背景图片、文字颜色或阴影等),即不会影响页面布局的属性,则浏览器会跳过布局,但仍将执行绘制。

3、性能最好之不重拍也不重绘
管道运行方式
JS/CSS —> Style —> Composite


此过程不会重排重绘,仅仅是进行合成,也就是修改 transform 和 opacity 属性更改来实现动画,性能得到较大提升,最适合于应用生命周期中的高压力点,例如动画或滚动。

尽量使用 transform 和 opacity 属性更改来实现动画

性能最佳的像素管道版本会避免 Layout 和 Paint

为了实现此目标,需要坚持更改可以由合成器单独处理的属性。常用的两个属性符合条件:transform 和 opacity。

另外:想知道每种 CSS 属性的更改是否会触发 Layout,Paint,Composite,可以通过 csstriggers.com 查看。

有一种能有效减小 Layout 和 Paint 的方法是将元素提升,像 Photoshop 中层的概念一样,样式也有层的概念,不同的层根据不同顺序叠加起来,通过 Composite 最终显

示出来。在每个层中对这个层进行 Layout 或者 Paint 是不会影响其他层的,一般会根据整个页面的语义将页面分为几个层。

提升元素还有一个好处就是会将动画从 CPU 转移到 GPU 来完成,来实现硬件加速。

尽量避免 Layout

让 DOM 脱离文档流再对其进行操作,所有操作完成后添加进文档流,这样可以将重排及重绘的次数降低到一次或两次(脱离文档流及回归文档流的时

候),以下方法可以让元素脱离文档流:

  • 隐藏元素 —— display: none;(事实上 display:none 不会让元素出现在 layout tree 中)。
  • 使用 DocumentFragment(推荐,只有一次 re-flow)。
  • 将原始元素拷贝到一个脱离文档的节点中,修改这个副本,完成后再替换掉原始元素。

事件委托

利用事件冒泡的机制来处理子元素事件的绑定,将子元素的 DOM 事件,交由它们的父元素来进行处理,可以有效降低页面的开销 —— 由于事件的冒泡

特性,只需要给父组件添加一个监听事件,就能够捕获到冒泡自子元素的事件,再通过 e.target 来获取真正被操作的子元素。

使用 requestAnimationFrame

在某个单个帧中,有可能发生这种情况,在某一帧中会被多次触发某个事件(比如 scroll),这个事件又会频繁的触发样式的修改,导致可能需要多次

Layout 或者 Paint,这是一种浪费,过于频繁的 Layout 和 Paint 会造成卡顿,而且实际上一帧中并不需要重复 Layout 或者 Paint 那么多次。

这个时候就可以用到 rAF 了,先放上一段 MDN 上对 rAF 的解释:

window.requestAnimationFrame() 方法告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调

函数作为参数,这个回调函数会在浏览器重绘之前调用。

在下一次重绘之前调用,理解: 重点不在于“重绘之前” 而是“下一次” 是下一次!不是本次!

简单来说,rAF的作用就是将传给rAF的回调函数,安排在下一帧的一开始执行。这样就能保证这个回调函数最先执行,并且因为

绝大多数浏览器都是60fps,所以rAF自带截流效果。

在浏览器的一轮事件循环中,会有 task -> microtask -> UI render,这么的一个循序,rAF 将回调函数放在下一帧的

开头,就是已经让其所在的那一轮的 UI 先 render,然后再在下一帧的最开始去执行.

理解: 开头,就是已经让其所在的那一轮的 UI 先 render,然后再在下一帧的最开始去执行。
在调用 rAF 时,有一点切记:不要在 rAF 的回调函数中先修改样式,再查询样式,这样就失去了 rAF 的作用。可以将对样式的查询提前到回调函数中或者 rAF 中尽量靠前的位置。

参考链接🔗:

30帧、60帧、120帧对画面影响有多大?
useEffect的执行时机
how to write fast memory-officient js