8

上篇文章《网站性能优化——CRP》已经介绍过网站性能优化中的关键渲染路径部分,相当于从一个“宏观”的角度去优化性能,当然,这个角度也是最重要的优化。本篇就从一个“微观”的层面去优化——浏览器渲染。

在视频领域,电影、电视、数字视频等可视为随时间连续变换的许多张画面,而帧则指这些画面当中的每一张。——维基百科

网页上来说,其实就是指浏览器渲染出的页面。目前大多数设备的屏幕刷新频率为60次/秒(60fps),每一帧所消耗的时间约为16ms(1000 ms / 60 = 16.66ms),但实际上,浏览器还有一些整理工作要做,因此开发者所做的所有工作需要在10ms内完成。

如果不能完成,帧率将会下降,网页会在屏幕上抖动,也就是通常所说的卡顿,这会对用户体验产生严重的负面影响。所以如果一个页面中有动画效果或者用户正在滚动页面,那么浏览器渲染动画或页面的速率也要尽可能地与设备屏幕的刷新频率保持一致,以保证良好的用户体验。

像素管道

提高帧率,其实就是优化浏览器渲染页面的过程。当你在工作时,需要了解并注意五个主要的区域,这些区域是你能在最大程度上去控制的地方,当然,也就是优化性能、提高帧率的地方。
clipboard.png

  • JavaScript:一般情况下,我们会使用JS去处理一些导致视觉变化的工作,比如动画或者增加DOM元素等。当然,除了JS,还有其他一些方法,比如:CSS Animations、Transitions、 Web Animation API

  • Style calculations:这个过程是根据匹配选择器(.nav > .nav-item)计算出哪些CSS规则应用在哪些元素上面的过程

  • Layout:浏览器知道对一个元素应用哪些规则之后,就可以开始计算这个元素占据的空间大小及其在屏幕上的位置

  • Paint:绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包含了元素的每个可视部分。绘制一般是在多个上完成的

  • Compositing(合成):由于页面的不同部分可能被绘制到多个上,因此它们需要按照正确的顺序绘制到屏幕上以正确渲染页面

像素管道的每个部分都有可能产生卡顿,因此,准确了解你的代码会触发管道的哪些部分十分重要。
帧不一定都会经过管道每个部分的处理。实际上,在改变视觉呈现时,针对指定帧,管道的运行通常有三种方式:

  • JS / CSS > Style > Layout > Paint > Composite
    clipboard.png当改变了某个元素的几何属性(如width、height,或者表示位置的left、top等)——即修改了该元素的“布局(layout)”属性,那么浏览器将会检查所有其他元素,然后对页面进行“重排(reflow)”。任何受到影响的区域都需要重新绘制,然后进行合成。

  • JS / CSS > Style > Paint > Composite
    clipboard.png当改变了只与绘制相关的属性(如背景图片、文字颜色或阴影等),即不会影响页面的布局,则浏览器会跳过布局阶段,但仍需要执行绘制、合成。

  • JS / CSS > Style > Composite
    clipboard.png当改变了一个既不需要“重排”也不需要“重绘”的属性(如transform),则浏览器将跳过布局、绘制阶段,直接执行合成。

浏览器渲染优化

JavaScript

使用 requestAnimationFrame

requestAnimationFrame应该作为开发者在创建动画时的必备工具,它会确保JS尽早在每一帧的开始执行。

之前我们可能看到过很多用setTimeoutsetInterval创建的动画,比如老版本的jQuery。但是使用这两个函数创建的动画效果可能不够流畅,JS引擎在安排这两个函数时根本不会关注渲染通道,参考《Html5 Canvas核心技术》中的论述:

1.即使向其传递毫秒为单位的参数,它们也不能达到ms的准确性。这是因为javascript是单线程的,可能会发生阻塞。
2.没有对调用动画的循环机制进行优化。
3.没有考虑到绘制动画的最佳时机,只是一味地以某个大致的事件间隔来调用循环。

使用 Web Worker

前面讨论过刷新一帧消耗的最佳时间大概在10ms左右,但是一帧里面通常又包括JS处理、样式处理、布局、渲染等等,所以JS执行的时间最好控制在3~4ms。JS在浏览器的主线程上运行,如果运行时间过长,就会阻塞样式计算、布局等工作,这样可能导致帧丢失。

许多情况下,可以将纯计算性的工作移到Web Worker,比如,不需要访问DOM的时候。数据操作或者遍历(如排序或搜索)往往很适合这种模型,加载和模型生成也是如此。

使用Timeline分析JS

当觉察到页面有卡顿的时候但又不知道是哪部分的JS造成的,这时可以打开Timeline录制时间轴,查看、分析是哪个地方的JS造成了页面卡顿,然后做针对性的JS优化。有关Timeline的使用,请参考《Chrome DevTools - Timeline》

样式

计算样式(computing styles)的第一部分是创建一组匹配选择器,以便浏览器计算出给指定元素应用哪些类、伪选择器和 ID。第二部分涉及从匹配选择器中获取所有样式规则,并计算出此元素的最终样式。
在当前的Chrome渲染引擎中,用于计算某元素计算样式的时间中大约有 50% 用来匹配选择器,而另一半时间则用于从匹配的规则中构建 RenderStyle。

降低选择器的复杂度:能写出高效率选择器的前端开发者本来就不多,又加上当前Less和Sass的普及,一些前端开发者对Less、Sass的滥用,导致编译后的css选择器有时候甚至能达到六七层嵌套,这大大增加了浏览器计算样式所消耗的总时间。
最理想的状态是每个元素都有一个唯一的id,这样选择器最简单也是最高效的,可是我们知道这是不现实的。但是,遵循一些指导原则依然能让我们写出较为高效的CSS选择器:Writing efficient CSS selectors

布局

尽可能避免布局操作

在修改CSS样式时,心里要清楚哪些属性会触发布局操作,能避免则避免。考虑到实际的开发情况,几乎避免不了啊~~如果无法避免,则要使用Timeline查看一下布局要花多长时间,并确定布局是否会造成性能瓶颈。如果布局消耗时间过多,则要从布局前面的JS和样式阶段查找一下原因,并做进一步的优化。
想知道哪些CSS属性会触发布局、绘制或合成?请查看CSS触发器

优先使用flexbox布局

如果用定位、浮动和flexbox都能达到相同的布局效果,在浏览器兼容的情况下,优先使用flexbox布局,不仅因为其功能强大,更是因为其性能在布局上更胜一筹。

避免强制同步布局

将一帧绘制到屏幕上会经历以下顺序:
clipboard.png
首先执行JS,然后计算样式,然后布局。但是,某些JS有可能强制浏览器提前执行布局操作,变成 JS > Layout > Styles > Layout > Paint > Composite,这被称为强制同步布局(Forced Synchronous Layout)

用一个demo来说明一下FSL:

clipboard.png

点击Trigger按钮,改变上面三个按钮的宽度,index.js内容如下:

1. var element1 = document.querySelector('.btn1');
2. var element2 = document.querySelector('.btn2');
3. var element3 = document.querySelector('.btn3');
4. var triggerBtn = document.querySelector('.trigger');
5. triggerBtn.addEventListener('click', function trigger(){
6.   // Read
7.   var h1 = element1.offsetWidth;
8.   // Write (invalidates layout)
9.   element1.style.width = (h1 * 2) + 'px';
10.
11.   // Read (triggers layout)
12.   var h2 = element2.offsetWidth;
13.   // Write (invalidates layout)
14.   element2.style.width = (h2 * 2) + 'px';
15.
16.   // Read (triggers layout)
17.   var h3 = element3.offsetWidth;
18.   // Write (invalidates layout)
19.   element3.style.width = (h3 * 2) + 'px';
20. });

clipboard.png

可以看到,读取offsetWidth属性会导致layout。但是,要注意的是,在 JS 运行时,来自上一帧的所有旧布局相关的值是已知的,并且可供查询。所以,在Timeline中看到第7行代码只是触发了Recalculate Style事件,并未触发Layout事件。当JS执行到第12行代码的时候,为了获取element2.offsetWidth,浏览器必须先执行计算样式(因为第9行代码改变了element1的width属性),然后执行布局,才能返回正确的宽度,第17行代码也是如此。这是不必要的,而且可能导致很大的时间开销。JS执行到第19行时,触发最终的Recalculate Style事件和Layout事件,渲染出新的一帧。

避免强制同步布局:先读取布局属性,然后批量处理样式更改。

...
6. // Read
7. var h1 = element1.clientHeight;
8. var h2 = element2.clientHeight;
9. var h3 = element3.clientHeight;
10.
11. // Write (invalidates layout)
12. element1.style.height = (h1 * 2) + 'px';
13. element2.style.height = (h2 * 2) + 'px';
14. element3.style.height = (h3 * 2) + 'px';

// Document reflows at end of frame

图片描述

可以看到,先读取布局属性,然后批量处理样式更改,只会导致最终的Layout,避免了FSL。

绘制与合成

当在页面上进行交互时,想知道哪些区域被重新绘制了?打开DevTools的副面板,切换到Rendering,勾选“Paint Flashing”:
clipboard.png

交互发生后,重新绘制的区域会闪烁绿色:
clipboard.png

绘制并非总是绘制到内存中的单个图像上。实际上,如果必要,浏览器可以绘制到多个图像(层)上。这种方法的优点是,定期重绘的元素,或者通过动画变形在屏幕上移动的元素,可以在不影响其他元素的情况下进行处理。这和图像处理软件Photoshop、Sketch等层的概念是类似的,各个层可以在彼此的上面处理并合成,以创建最终图像。

创建新层的最佳方式是使用will-change CSS 属性,当其属性值为transform时,将会创建一个新的合成器层(compositor layer)

.moving-element {
  will-change: transform;
}

对于不支持will-change属性的浏览器,可以使用以下css做兼容处理:

.moving-element {
  transform: translateZ(0);
}

需要注意的是:不要创建太多层,因为每层都需要内存和管理开销。如果你已将一个元素提升到一个新层,最好使用 DevTools 确认一下这样做能带来性能优势。请勿在不分析的情况下提升元素

最后说一下如何使用Timeline了解网页中的层。
图片描述

勾选Paint,然后录制Timeline,然后点击单个帧,这时详情选项里面多了个“layer”选项卡,切换到此选项卡。展开左侧#document,即可看到页面里面有多少个层(layer),单击每个层时,右侧还会显示这个层被创建的原因。
如果在性能关键操作期间(比如滚动或动画)花了很多时间在合成上(应当力争在4-5ms左右),则可以使用此处的信息来查看页面有多少层、创建层的原因,进一步去管理页面中的层数。


References


无名小贝勒
5.7k 声望324 粉丝

引用和评论

1 篇内容引用
0 条评论