前端页面性能优化小记

​ 我之前在写学校艺术中心的项目页面的时候要求有大量的动画,一开始我写的页面动画非常不流畅,有很多卡顿,因此我通过在慕课网上学习了让你页面速度飞起来 Web前端性能优化以及在优达学堂(我是通过在谷歌开发者帮助上找到这个网址的)来了解一些关于现代浏览器页面渲染的原理和过程,当然还有The Applied Science of Runtime Performance - Chrome Dev Summit 2014 (Paul Lewis) 、张鑫旭的回流与重绘:CSS性能让JavaScript变慢?如何不择手段提升scroll事件的性能?(最早来自知乎?)以及该文章内引用的其它文章…不再一一列举。为此我还在小组会上做了一定的分享,因此我将在下面记录下所学的内容。

​ 这可能不是一篇正经的博文,这是自己总结写给自己看的,因为可能只有我能看得懂hhh,一起之前分享后的整理,但是看完优达学堂确实能比较好地理解整个过程(墙裂推荐)。

前端几个重要线程

GUI渲染线程

· 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。

· 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行

· 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

JS引擎线程

· 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)

· JS引擎线程负责解析Javascript脚本,运行代码。

· JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序

· 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

事件触发线程

· 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)

· 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中

· 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理

· 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)

定时触发器线程

· 传说中的setInterval与setTimeout所在线程

· 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)

· 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)

· 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。

异步http请求线程

· 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求

· 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

新建DOM的过程(浏览器如何构建关键帧)

​ 当浏览器向服务器发出Get请求获取页面

image1

​ 服务器会向浏览器返回html页面,这是原始的文本

image2

​ 浏览器通过解析其中的dom结点从而形成dom树

image3

image4

​ 浏览器在构造dom树的同时又会请求该页面的css样式文件,这个过程在chrome的develop tool中叫recalculate Styles

image5

​ dom树结合了css有了样式,构成了新的树叫做渲染树

image6

​ 渲染树没有了head和script,如果display:none也会从渲染树除去,如果有:after等伪类,即使没有出现在DOM树,也会添加到渲染树中。一般来说只有显示在网页上的元素才会添加到渲染树中

image7

​ 下一个过程叫做layout,也叫做reflow(回流),即对元素的规模尺寸,布局进行页面上的初步构建,注意这是一种网络布局模型,意味着某个元素可以影响到其他元素

image8

​ 下一个过程是矢量到光栅的转化,因为电脑屏幕实际上是由一个个格子的像素构成的,这是浏览器的内部操作,我们不需要了解太多

image9

​ 再下一步则是到了重绘这个过程(paint),其将元素的外观绘制在页面上

image10

image11

​ 其中有一步drawBitmap,浏览器将内容解码成内存,图片的绘制实际上是一个一个像素点解码绘制的,像下面这样:

image12

​ 上面讲的是一个图层所要做的事情,因此还有一个过程是的对图层进行合成

image13

image14

image15

​ 所有这些都是在CPU上发生的,图层本身和其图块将上传到GPU中,同样,这一流程也包含在composite Layers部分,最后GPU将根据指示将图片显示到屏幕上。但我们开发者无法控制这一过程。

总结如下

1.获取DOM后分割为多个图层

2.对每个图层的节点计算样式结果(Recalculate style—样式重计算)

3.为每个节点生成图形和位置(layout—回流和重布局)

4.将每个节点绘制填充到图层位图中(Paint Setup和Paint—重绘)

5.图层作为纹理上传至GPU

6.复合多个图层到页面上生成最终屏幕图像(Comoposite Layers—图层重组)

对于开发者而言

对于我们开发者来说,通常帧是这样的,你需要了解管道的概念:

image17

你会通过js来改变样式,并导致外观变化,或者添加DOM

但是并不一定需要js,也可以使用CSS transform、transition来改变,甚至使用最近的Web animations API改变外观变化。

但是通过js改变不一定触发所有管道元素

Javascript->style->layout->paint->composite

Javascript->style->paint->composite

Javascript->style->composite

每次改变都涉及到了style,改变不同属性影响到了不同的管道元素,进而影响到应用的性能

其中有两个重要的概念就是回流(layout/reflow)重绘(paint)

回流

​ 当render tree中的一部分或全部因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)

​ 当页面布局和几何属性改变时就需要回流

重绘

​ 当render tree中的一些元素需要更新属性,而这些属性影响元素的外观,风格,而不会影响布局的,比较background-color。则称为重绘

​ 这里有一些有趣的视频可以直观地看到相关步骤

​ 1. http://www.youtube.com/watch?v=nJtBUHyNBxs

​ 2. http://www.youtube.com/watch?v=ZTnIxIA5KGw

​ 3. http://www.youtube.com/watch?v=dndeRnzkJDU

基本思想

​ 因此根据页面渲染的原理和管道的过程,页面性能优化的基本思想如下:

​ 1.避免使用触发重绘、回流的CSS属性

​ 2.将重绘、回流的影响范围限制在单独的图层之内,那么这个DOM元素的重绘和回流的影响只会在这个图层中。

只触发重绘的属性

color border-style border-radius visibility text-decoration background background-image background-position background-repeat background-size outline-color outline outline-style outline-width box-shadow

触发页面重布局的属性

盒子模型相关属性

定位属性及浮动

改变节点内部文字结构

width height padding margin display border-width border min-height

top bottom left right position float clear

text-align overflow-y font-weight overflow font-family line-height vertical-align white-space font-size

chrome创建图层的条件

1.3D或透视变化CSS属性(perspective transform will-change)

2.使用加速视频解码的节点

3.拥有3D(WebGL)上下文或加速的2D的上下文(canvas)

4.混合插件(Flash)

5.对自己的opacity做CSS动画或使用一个动画webkit变换的元素

6.拥有加速CSS过滤器的元素 (translate3D)

7.元素有一个包含复合层的后代节点(一个元素拥有一个子元素,该子元素在自己层里)

8.元素有一个z-index较低且包含一个复合层的兄弟元素(换句话就是该元素在复合层上面渲染)

实战优化点

1.用translate替代top改变

2.用opacity 替代visibility

3.不要一条一条地修改DOM的样式,预先定义好class,然后修改DOM的className
4.把DOM离线后修改,比如:先把DOM给display:none(有一次Reflow),然后你修改100次,再把它显示出来

5.不要把DOM结点的属性值放在一个循环里当成循环里的变量

6.不要使用table布局,可能很小的一个改动会造成整个table的重新布局

7.动画实现的速度选择

8.对于动画新建图层

9.启用GPU硬件加速

以上是别人总结的……下面我来讲讲我踩得坑

1.背景图片设置默认高度。因为图片没加载出来的时候如果没有设置默认高度,会导致图片加载的时候时时回流,因此每时每刻都在消耗性能。

2.用插件skrollr代替background-attachment :fixed做背景视差。skrollr的原理是改变图片的transform属性,而background-attachment会时时触发回流重绘。

3.FILP(First、Last、Invert、Play)思想。其原理是获取动画开头和结尾的状态,然后用transform去做动画。

CQN88`1T02C5I}MZ000Q}DM

4.tranform会新建图层,but图层不能被滥用!!否则会造成composite的高消耗

针对动画优化JS

帧数

​ 相当一部分的浏览器的显示频率是16.7ms(1000/60),意味着动画要到达到60帧/秒,因此浏览器渲染每帧的时间要不得超过16.7ms,但是浏览器除了渲染还有很多工作要做,因此我们应该控制在10ms-12ms之内完成渲染工作

image32

​ 显示器16.7ms刷新间隔之前发生了其他绘制请求(setTimeout),导致所有第三帧丢失,继而导致动画断续显示(堵车的感觉),这就是过度绘制带来的问题。

requestAnimationFrame

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

Web worker

分离无页面渲染无关的逻辑计算

image33

函数节流和函数防抖

针对scroll事件中的回调,思路之一是对事件进行“稀释”,减少事件回调的执行次数。

函数去抖(debounce):让函数只有在过完一段时间后并且该段时间内不被调用才会被执行

函数节流(throttle):让函数在指定的时间段内周期性地间断执行

例子1

例子2 - 这篇文章讲的非常到位咯

不知道从哪截的图(侵删)

image34

image35

避免强制重排(StoppingForced Synchronous Layout)

例子

image36

image37