[关闭]
@FunC 2017-04-30T16:42:00.000000Z 字数 4802 阅读 3008

浏览器渲染优化笔记

前端 优化


(本来想写一篇全面的渲染优化的笔记的,发现学问很深,所以先把这两天学的做个笔记)

页面的呈现

要优化渲染,首先要知道页面是怎么呈现的
粗略的可以分为下面几步(括号内为chrome中timeline/performance对应的名字)
1. 构建DOM树(parseHTML)
2. 构建CSSOM树(Recalculate Styles)
3. 将DOM树和CSSOM树组合成Render树
4. 布局(layout):根据Render树计算每个元素的位置、大小等信息,给出矢量数据
5. 绘制(paint):从矢量数据转换成栅格化数据(即填充到像素格子中)+绘制位图(解析图片数据并缩放)
6. 图层复合(composite):在CPU上绘制单独的图层(layer),发送给GPU,让其合成并展示

渲染流程

渲染的流程是分析渲染的关键,请好好观察这个流程:

javascript > style > layout > paint > composite

其中触发部分不一定是js,也可以是css animation/web animation api
值得注意的是,layout>paint>composite的过程
这三步中前面的步骤触发了,后面的一定会触发。但从js开始的整个过程不一定完整走完。下面是一些辨析,可跳过:

  1. 改变了layout相关属性(如width),则全部触发
  2. 仅改变了paint相关属性(background-color),不触发layout
  3. 仅改变图层相关属性,不触发layout和paint(transform,opacity)
    注意:三种情况中style每次都会进行计算,因为改变了style(即样式属性发生了变化)
    区别:使用flexbox时,resize不用重新计算style,因为style属性没有变化
    区别2:如果有resize handler触发style变化,或者触发media query使style变化,都要进行style计算
    可到csstriggers查看哪些CSS属性会触发layout、paint和composite

JavaScript优化

首先,不要浪费时间在微优化上(如使用for循环还是while循环?),因为你不知道js引擎怎么解析你的代码。

JavaScript和渲染主要有关的方面有:
1. 改变样式
2. 制作动画(实质是一系列的改变样式)

为达到60fps的流畅动画、去除掉浏览器的额外工作后,每帧只剩下约10ms可供准备。不要忘了这10ms里还包括style,layout,paint,composite等。(更多信息详见后文app生命周期部分)

不要用setTimeout和setInterval做动画

16ms每帧,很容易就能联想到定时器:每16ms执行一次动画操作。
然而定时器时间的精度取决于浏览器的内置时钟更新频率,对于IE8及以前是15.6ms,IE9后是4ms。
此外还有异步队列的问题(要等主线程为空时异步队列任务才能入栈)

拥抱requestAnimationFrame(rAF)

说在前面:该API从IE10才开始支持
这是浏览器专门用于动画的api,通过该API调用的回调函数会在浏览器对页面画面刷新前(即每一帧前)执行,保证了每一帧前完成样式改变,达到60fps的基本要求。

值得注意的是,回调函数调用时机是页面画面刷新前。只有浏览器自己才知道什么时候刷新,所以某种意义上来说触发时机的不确定的。
如setTimeout、promise和requestAnimationFrame按顺序运行,唯一能确定的是promise在setTimeout之前触发。
另一个例子就是使用 jsbin 时,如果关闭了output,rAF是不会触发的。

rAF应用:推迟代码

前置知识:
1. UI线程(除CSS动画外)与js线程互斥
2. 与样式相关操作一般性能耗费巨大(如触发强制同步layout)

根据1和2,与样式相关操作最好刚好在每一帧之前完成,即应该放在rAF里。在有异步任务时性能提升更明显(如更早发出ajax请求,则更早收到response)

rAF应用:解耦代码

以无限滚屏为例:
如果将大量操作绑定在scroll事件上,将会频发触发回调,造成页面卡顿。
正确做法是scroll只改变标记,通过定时检查标记来判断是否需要执行回调。

  1. var didScroll = false;
  2. $(window).scroll(function() {
  3. didScroll = true;
  4. });
  5. setInterval(function() {
  6. if ( didScroll ) {
  7. didScroll = false;
  8. // Check your page position and then
  9. // Load in more results
  10. }
  11. }, 250)

当然这里的定时器也可用rAF代替

使用web worker

虽然js是单线程,web worker提供了多线程的能力,可将长时间js运算分配给web worker完成。线程之间通过worker.postMessage和监听message事件通讯。
值得注意的是,传递的信息是复制的而不是共享的。如传输对象时,会自动JSON编码/解码
处于线程安全的考虑,web worker只能使用 JavaScript 功能的子集:

Worker 无法使用:

更多内容请阅读Web Workers 的基本信息

避免强制同步layout(FSL)

以下面的代码为例:

  1. for(var i = 0; i < DOMList.length; i++){
  2. var oldWidth = DOMList[i].offsetWidth;// 第一次读取时不会触发layout,利用的是上一帧layout的信息
  3. DOMList[i].style.width = oldWidth + 10 + 'px';// 改变style后,元素之间的位置信息不明,故第二次读取offset的时候需要触发强制同步layout
  4. }

这段代码的性能非常之差,关键在于offsetWidth的信息是从layout的计算结果中获得的。
当修改了style之后,浏览器就不知道位置信息了,需要在js阶段强制layout来得到offsetWidth的结果,而layout是很耗费性能的。
正确的做法是批量读取完之后再对style进行修改。对于可预测的style变化,不要每次都通过offsetWidth这类api获取,可进行手动计算。

合成与绘制

composite是渲染流程的最后一步

javascript > style > layout > paint > composite

如果只触发composite,就能避免触发消耗巨大的layout和paint

想象一个侧边栏组件:如果侧边栏和主页面属于同一个图层的话,每次侧边栏拉入,都会覆盖掉主页面的内容。而折叠侧边栏时,因为不知道侧边栏下面是什么,只能重新layout+paint。
当侧边栏属于独立的图层时,GPU只需要把该图层移动到合适的位置即可,无需layout和paint。

提升为独立图层的原因有下面这些(可通过chrome开发作者工具中的layer查看原因)

其中前2点是最常用的,3d变换兼容性更高。
也许你会说,既然图层这么好,那干脆全部都加上好啦:

  1. *{
  2. will-change:transform;
  3. }

世上没有免费的午餐,创建图层并保存图层信息也会带来资源的消耗。滥用图层将消耗大量的内存资源。(这是空间和时间的取舍)

关于will-change

这个属性使用起来很爽,但使用恰当并不容易。其设计目的是作为最后的优化手段,而不是用来预防性能问题的。所以最佳实践是当元素变化之前和之后通过脚本来切换 will-change 的值。
同时这个属性的原理是提前告诉浏览器可能发生变化的属性,让其提前完成优化工作。所以要预留时间给浏览器优化。当然,当页面主要用途就是动画切换(如相册类),且画面大而复杂的时候,直接在样式设置will-change是合理的。

参考will-change在MDN的词条

最后提一下web APP的生命周期

虽然讨论了很久怎么做到60fps动画,但其实不总需要保持60fps(如更改颜色方案、有滑入/滑出动画时)

网络应用的生命周期四阶段:RAIL(便于记忆、实际顺序是LIAR)

R:Response | 100ms
A:Animation | 16ms/帧(实际10~12ms)
I:Idle(闲置)| 50ms
L:Load | 1000ms

当运算时间超过了后方标注的时间,用户将能感知到卡顿

加载与闲置

加载(Load)完后,一般处于闲置(Idle)状态,等待用户去互动(Interact),此时是执行要推迟(defer)的任务的绝佳时间,从而达到1s加载完毕的感觉。

通常闲置时间为50ms

以新闻app的文章页面为例(有文本,图片,视频,评论区):
因核心目的为阅读新闻,所以文本必须先加载(用户会花时间阅读文本,留出idle时间)
闲置时间适宜加载的内容:image、videos、comments section等,
不适宜加载的内容:文本和基础且必要的功能

响应

对用户的操作作出反应
1. 简单的:点击按钮
2. 复杂的:会触发动画的(因为要求16ms/帧)

动画

优化方案例子: FLIP(First,Last,Invert,Play)

原理:
1. 一旦浏览器完成了性能消耗高的动画,逆向进行将变得简单。
2. 看起来高耗时的计算位置工作在click到animation begin之间的100ms(response可接受时间)内完成(相比于10ms内完成一帧其实较为宽松)
先计算一开始的位置(First),如可使用getBoundingClientRect()等;计算最后的位置,应用动画(配合opacity,因为性能优秀)

RAIL应用场景

  1. 菊花图是否应该在触发视频时发起请求?
    答案:否,首先请求时间可能超过16ms。即使不超过16ms,额外的时间也会拖长这一帧使其超过16ms。(这么小的资源完全可以在一开始就加载)

  2. 在idle时可加载的内容
    答:非首屏内容都可尝试进行加载(包括FLIP的计算)

尽管有一定的时间可以用于计算,但注意时间不是无限的。减少影响的元素的数量有助于优化计算时间

总结:

  1. 尽可能使用CSS动画(不占用JavaScript线程资源)
  2. 尽量减少layout和paint(包括强制同步layout)
  3. 使用rAF推迟、解耦动画相关代码
  4. 对于长时间的同步任务考虑使用web worker
  5. 合理分配图层(提升的时机、数量)
  6. 避免隐式合成图层(额外的图层开销)
  7. 合理利用web app的生命周期,达到感官上的流畅(如利用闲置时间下载后续内容,利用作出响应前的100ms完成所需计算等)

拓展阅读

google的性能表现文档
Javascript高性能动画与页面渲染
webkit中会触发layout的api
Web Workers 的基本信息
一篇文章说清浏览器解析和CSS(GPU)动画优化
csstriggers

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注