网站性能优化—浏览器渲染

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

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

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

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

像素管道

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

  • JavaScript:通常状况下,咱们会使用JS去处理一些致使视觉变化的工做,好比动画或者增长DOM元素等。固然,除了JS,还有其余一些方法,好比:CSS Animations、Transitions、 Web Animation APIgithub

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

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

  • 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

相关文章
相关标签/搜索