css渲染流程及动画深入理解和优化方案

2019-10-13 04:20 来源:未知

CSS3 动画卡顿性能优化解决方案

2018/02/06 · CSS · 动画

原文出处: 趁你还年轻   

 图片 1

最近在开发小程序,与vue类似,它们都有生命周期这回事。

onLoad 监听页面加载
onReady 监听页面初次渲染完成
onShow 监听页面显示

到底是什么意思?

所以这又触碰到了我的知识盲区,不过项目在磕磕绊绊中完成的差不多了,但是遇到了CSS3动画渲染的性能问题,所以我也是被逼的,再回过头来从浏览器渲染网页的流程出发,去找动画卡顿的症结。

浏览器渲染网页的流程如下:

使用 HTML 创建文档对象模型(DOM)
使用 CSS 创建 CSS 对象模型(CSSOM)
基于 DOM 和 CSSOM 执行脚本(Scripts)
合并 DOM 和 CSSOM 形成渲染树(Render Tree)
使用渲染树布局(Layout)所有元素
渲染(Paint)所有元素

可以结合Alon的这篇前端性能优化和安卓开发者选项的显示页面布局。

安卓开发者选项的显示页面布局

如何判断手机app是native,webview还是hybird?
简单说下,app中的一大块是白色的没有红线标记出来的,但是上面有按钮,图片等时,就是webview,也就是通过一个伪浏览器去请求到的数据,断网时打开app没有任何东西显示在上面

图片 2

onLoad 监听页面加载
在渲染完界面之后,也就是通过.json中的配置项生成native界面后,开始渲染webview的部分,一个页面只会调用一次。
onReady 监听页面初次渲染完成
一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互。
onShow 监听页面显示
每次打开页面都会去调用其中的函数。

网页的分层机制

一个网页是由多个层展示的,然后再将这些层合并成一个层,当dom或者样式发生变化时,GPU能够缓存一些不变的内容,将要变化的层与缓存层再合成,提高渲染效率,因此在做动画时要让GPU参与进来,提高动画性能

为什么会卡顿?

有一个前提必须要提,前端开发者们都知道,浏览器是单线程运行的。
但是我们要明确以下几个概念:单线程,主线程和合成线程。

虽然说浏览器执行js是单线程执行(注意,是执行,并不是说浏览器只有1个线程,而是运行时,runing),但实际上浏览器的2个重要的执行线程,这 2 个线程协同工作来渲染一个网页:主线程和合成线程。

一般情况下,主线程负责:运行 JavaScript;计算 HTML 元素的 CSS 样式;页面的布局;将元素绘制到一个或多个位图中;将这些位图交给合成线程。

相应地,合成线程负责:通过 GPU 将位图绘制到屏幕上;通知主线程更新页面中可见或即将变成可见的部分的位图;计算出页面中哪部分是可见的;计算出当你在滚动页面时哪部分是即将变成可见的;当你滚动页面时将相应位置的元素移动到可视区域。

5.CPU VS GPU

图片 3

ALU 面积越大,计算能力越强,

ALU计算单元越多,吞吐量就越大

相同: 两者都有总线和外界联系,有自己的缓存体系,以及运算单元,两者都为了完成计算而生 不同: CPU主要负责操作系统和应用程序的逻辑运算,GPU处理显示相关的数据处理 GPU运算更快,GPU的活CPU一般都能干,但是效率低下

那么为什么会造成动画卡顿呢?

原因就是主线程和合成线程的调度不合理。

下面来详细说一下调度不合理的原因。

在使用height,width,margin,padding作为transition的值时,会造成浏览器主线程的工作量较重,例如从margin-left:-20px渲染到margin-left:0,主线程需要计算样式margin-left:-19px,margin-left:-18px,一直到margin-left:0,而且每一次主线程计算样式后,合成进程都需要绘制到GPU然后再渲染到屏幕上,前后总共进行20次主线程渲染,20次合成线程渲染,20 20次,总计40次计算。

主线程的渲染流程,可以参考浏览器渲染网页的流程:

使用 HTML 创建文档对象模型(DOM)
使用 CSS 创建 CSS 对象模型(CSSOM)
**基于 DOM 和 CSSOM 执行脚本(Scripts)
合并 DOM 和 CSSOM 形成渲染树(Render Tree)
使用渲染树布局(Layout)所有元素
渲染(Paint)所有元素**

也就是说,主线程每次都需要执行Scripts,Render Tree ,Layout和Paint这四个阶段的计算。

而如果使用transform的话,例如tranform:translate(-20px,0)到transform:translate(0,0),主线程只需要进行一次tranform:translate(-20px,0)到transform:translate(0,0),然后合成线程去一次将-20px转换到0px,这样的话,总计1 20计算。

可能会有人说,这才提升了19次,有什么好性能提升的?

假设一次10ms。

那么就减少了约190ms的耗时。

会有人说,辣鸡,才190ms,无所谓。

那么如果margin-left是从-200px到0呢,一次10ms,10ms*199≈2s。

还会有人说,辣鸡,也就2s,无所谓。

你忘了单线程这回事了吗?

如果网页有3个动画,3*2s=6s,就是6s的性能提升。
由于数据是猜测的,所以暂时不考虑其真实性,文章后面我使用chrome devtools的performance做了一个实验。

要知道,在”客户至上”的今天,好的用户体验是所有产品的必须遵守的一条规则,无论是对于开发者还是产品经理,追求极致的性能都是我们打造一个好的产品所必备的品质。

可能看了我的略不专业的分析后,大家对主线程,合成线程以及它们在2种性能不同动画方案上的工作流程还不是很了解,可以去看一篇翻译过来的博客(英文原版链接已经失效了):深入浏览器理解CSS animations 和 transitions的性能问题

这篇文章完美讲述了浏览器主线程和合成线程的区别,并且举了一个高度从100px变化到200px的2种动画方案的对比,对主线程和合成线程的整个工作流程做了很详尽的讲解,真心建议认真阅读一遍。

回过头来总结下,css3动画卡顿的解决方案:
在使用css3 transtion做动画效果时,优先选择transform,尽量不要使用height,width,margin和padding。

transform为我们提供了丰富的api,例如scale,translate,rotate等等,但是在使用时需要考虑兼容性。但其实对于大多数css3来说,mobile端支持性较好,desktop端支持性需要格外注意。


补充:为了增强本文的说服力,特地回家做了一个实验,代码如下。

JavaScript

<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Page Title</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> .margin-transition{ /* margin-left: 0; */ background: rgba(0,0,255,0.3); transition: margin-left 1s; } .transform-transition{ /* transform: translate(0,0); */ background: rgba(0,255,0,0.3); transition: transform 1s; } .common{ height: 300px; width: 300px; } </style> </head> <body> <div class="margin-transition common" id="marginTransition"> <p>transition:margin-left 1s</p> </div> <div class="transform-transition common" id="transformTransition"> <p>transition:tranform 1s</p> </div> <button id="control">见证奇迹</button> <script> var btn = document.getElementById('control'); var marginTransition = document.getElementById('marginTransition'); var transformTransition = document.getElementById('transformTransition'); btn.addEventListener("click",function(){ console.log(marginTransition.style,transformTransition.style) marginTransition.style.marginLeft = "500px"; transformTransition.style.transform = "translate(500px,0)" }) </script> </body> </html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Page Title</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    .margin-transition{
      /* margin-left: 0; */
      background: rgba(0,0,255,0.3);
      transition: margin-left 1s;
    }
    .transform-transition{
      /* transform: translate(0,0); */
      background: rgba(0,255,0,0.3);
      transition: transform 1s;
    }
    .common{
      height: 300px;
      width: 300px;
    }
  </style>
</head>
<body>
  <div class="margin-transition common" id="marginTransition">
    <p>transition:margin-left 1s</p>
  </div>
  <div class="transform-transition common" id="transformTransition">
      <p>transition:tranform 1s</p>
  </div>
  <button id="control">见证奇迹</button>
  <script>
      var btn = document.getElementById('control');
      var marginTransition = document.getElementById('marginTransition');
      var transformTransition = document.getElementById('transformTransition');
      btn.addEventListener("click",function(){
        console.log(marginTransition.style,transformTransition.style)
        marginTransition.style.marginLeft = "500px";
        transformTransition.style.transform = "translate(500px,0)"
      })
  </script>  
</body>
</html>

我将主要借助chrome devtools的performance工具对比二者的性能差异。
先来看margin动画,动态修改DOM节点的margin-left值从0到500px;。

JavaScript

transition: margin-left 1s;

1
transition: margin-left 1s;

图片 4
图片 5
图片 6

再来看下transform动画,动态修改DOM节点的transform值从translate(0,0)到translate(500px,0)。

JavaScript

transition: transform 1s;

1
transition: transform 1s;

图片 7

图片 8

 

 

图片 9

可能图片不是很好地能说明性能差异,那么我们来列一张耗时对比表,方便我们计算。

耗时 margin transform
Summery 3518ms 2286ms
Scripting 1.8ms 2.9ms
Rendering 22.5ms 6.9ms
Painting 9.7ms 1.6ms
Other 39.3ms 25.2ms
Idle( browser is waiting on the CPU or GPU to do some processing) 3444.4ms 2249.8ms
GPU使用率 4.1MB 1.7MB

通过上表我们可以计算出明margin,transform与transition组合实现CSS3动画效果时的性能差异参数。

关键性能参数 margin transform
实际动画耗时(总时间 减去 空闲时间) 73.6ms 36.2ms

计算得出,transform动画耗时约等于margin动画耗时的0.49倍,性能优化50%。

由于我对Other的所做的具体事情不是很清楚,所以这里的实际动画时间也有可能还要减掉Other中的时间,下表是我们减掉后的数据。

关键性能参数 margin transform
实际动画耗时(总时间 减去 其他时间和空闲时间) 34.3ms 11ms

计算得出,transform动画耗时约等于margin动画耗时的0.32倍,性能优化接近70%。

也就是说,无论我们减去还是不减去Other的时间,我们采用transform实现动画的方式都比margin动画快。

不精确的得出一个小结论:transform比margin性能好50%~70%

虽然会有50%~70%的性能提升,但是需要注意硬件差异,硬件好的情况下可能不能发现卡顿或者其他的一些性能上的问题。
例如在开发小程序的过程中,模拟器是位于desktop端的,因此它的硬件性能性能更好,例如CPU,GPU。但是一旦在mobile端运行,例如ios或者android上运行时,就可能会出现性能问题,这就是因为移动端的硬件条件逊于PC端导致的。

所以说,性能问题是一直存在的,只不过硬件差异会导致性能影响的程度不同。

所以我们再次回过头来,总结出css3动画卡顿的解决方案:
在使用css3 transtion做动画效果时,优先选择transform,尽量不要使用height,width,margin和padding。

That’s it !

参考:
http://sy-tang.github.io/2014…
http://jinlong.github.io/2017…
http://blog.csdn.net/yeana1/a…
https://www.jianshu.com/p/b70…
https://developers.google.com…
http://blogs.adobe.com/webpla…

1 赞 4 收藏 评论

图片 10

方案一 :面对别人写好的代码,使用requestAnimationFrame 推迟到下一帧执行

//Bad Code - 别人写好的代码

el1.addEventListener('click', function(){

    var h1 = el1.clientHeight;

    el1.style.height = (h1 * 2) 'px';

});

el2.addEventListener('click', function(){

    var h2 = el2.clientHeight;

    el2.style.height = (h2 * 2) 'px';

});

//Good Code - 优化代码

el1.addEventListener('click', function(){

    //Read

    var h1 = el1.clientHeight;

    //Write 推迟到下一帧再执行

    requestAnimationFrame(function(){

      el1.style.height = (h1 * 2) 'px';

    });

});

el2.addEventListener('click', function(){

    var h2 = el2.clientHeight;

    requestAnimationFrame(function(){

      el2.style.height = (h2 * 2) 'px';

    });

});


requestAnimationFrame(function(){

    $('#test').width();

})

我们的动画应该放在哪里?

应该放在onShow里,因为这样我每次打开都能看到动画。

4.不要以为单独的层是万能的,单独层内部的元素触发重排、重绘的条件,一样会只重排、重绘这一层

深层次理解重排与重绘

浏览器执行线程: 主线程 和 排版线程 

1.主线程: 通常情况下,主线程主要负责以下工作:运行JavaScript、计算HTML元素的CSS样式、布局页面、把页面元素绘制成一个或多个位图、把这些位图移交给排版线程

 2.排版线程: 通过GPU把位图绘制到了屏幕上

 3.重排与重绘 浏览器下载完页面中的所有资源(html、js、css、图片)

-> 解析成 DOM树和渲染树

-> DOM树表示页面结构,渲染树表示DOM节点如何显示

-> DOM树中的每一个需要显示的节点在渲染树种至少存在一个对应的节点(隐藏的DOM元素 disply值为none 在渲染树中没有对应的节点)

-> 渲染树中的节点被称为“帧”或“盒”,符合CSS模型的定义,理解页面元素为一个具有填充,边距,边框和位置的盒子

-> 一旦 DOM和渲染树构建完成,浏览器就开始显示(绘制)页面元素

-> 当DOM的变化影响了元素的几何属性(宽或高),浏览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。这个过程称为重排。

-> 完成重排后,浏览器会重新绘制受影响的部分到屏幕,该过程称为重绘

那么我们怎么避免重排和重绘给它们进行优化呢?

浏览器会把要引起重绘与重排的操作都塞到主线程队列里面,正准备执行优化后队列的时候,如果你做了一个读取width的操作,浏览器会全部放弃之前的优化,造成性能急剧下降

###两个问题

-- 开始绘制的时机

-- 绘制一帧的时间(16.7ms最完美)

方案二: 分离读写,减少Layout

在每一帧先做批量的读操作,再批量运行写操作

fastdom.js 3.1.使用读写分类的策略来优化

6.性能检测工具:Timeline或Performance 检测动画性能

对比一下两个动画

//1.不使用transform:引起重排和重绘

@keyframes run-around{

    0%{top: 0;left: 0;}

    25%{top: 0;left: 200px;}

    50%{top: 200px;left: 200px;}

    75%{top: 200px;left: 0;}

    100%{top: 0;left: 0;}

}

图片 11

//2.使用transform:不引起重排和重绘

@keyframes run-around{

    0%{transform: translate(0,0);}

    25%{transform: translate(200px,0);}

    50%{transform: translate(200px,200px);}

    75%{transform: translate(0,200px);}

    100%{transform: translate(0,0);}

}

//3.矩阵动画: 更高效

@keyframes run-around{

    0%{transform: matrix(1, 0, 0, 1, 0, 0);}   

    25%{transform: matrix(1, 0, 0, 1, 200, 0);} 

    50%{transform: matrix(1, 0, 0, 1, 200, 200);}

    75%{transform: matrix(1, 0, 0, 1, 0, 200);} 

    100%{transform: matrix(1, 0, 0, 1, 0, 0);}

}

图片 12

1.在整个动画的每一帧中,浏览器都要去重新布局,绘制页面,并把最新的位图对象加载到GPU2.根据定义,CSS的transform属性不会改变元素的布局,也不会影响到其周围的元素。它把元素当做一个整体看待——缩放整个元素、旋转整个元素或者移动整个元素。 浏览器只需在动画开始的时候生成这个元素的位图对象,并把它传递给GPU。在这之后,浏览器无需再做任何重新布局,绘制页面以及传递位图对象的操作了,相反,浏览器可以利用GPU擅长的绘制的特点来快速的在不同的位置,旋转或缩放同一个位图对象

transform: 节省了CPU进行Layout、Paint的时间((跳过),节省了CPU向GPU传输位图的时间

最后用矩阵matrix来代替转换成transform这样子又避免了动画开始的时候生成这个元素的位图图像,把性能做到极致

完美的Animation

-- 15FPS 有卡顿

-- 30FPS 感觉流畅

-- 60FPS 更舒服完美

-- 60FPS: 1s/60FPS = 16.7ms 表示1秒钟完成的60帧, 16.7ms/FPS (16.7毫秒,就要把一帧准备好)

总结

图片 13

1.webkit渲染html css

1-1.获取DOM 分割层 

1-2.计算CSS样式 

1-3.重排,放置dom的位置(layout) 

1-4.节点填充 重绘(paint)

 1-5.GPU生成纹理展现到页面(元素偏移、缩放)

 1-6.GPU参与网页合成层(compsite) => 屏幕最终图像 

【DOM子树渲染层(RenderLayer) -> RenderObject(元素) -> GraphicsContext】 

【Compositor -> 渲染层子树的图形层(GraphicsLayer) -> RenderLayer -> RenderObject】 【Compositor将所有的拥有compositing layer 进行合成,合成过程GPU进行参与。 合成完毕能够将纹理映射到一个网格几何机构之上,纹理能够以很低代价映射到不同的位置,而且还能够以很低的代价通过把他们应用到一个非常简单的矩形网格中进行变形,这就是 3D CSS 实现原理。】

 【GPU参与: CSS3D、webgel、transform、硬件加速】 

【硬件加速: ①.Texture,即CPU传输到GPU的一个BitMap位图 ②GPU能快速对Texture进行偏移、缩放、旋转、修改透明度等操作 开启硬件加速,让动画元素独立创建一个层,例如transform:translateZ(0) 】 

【Composite:GPU也是有限度的,不要滥用GPU资源生成不必要的Layer,留意意外生成的Layer】

 总结: 浏览器渲染: Load、Layout、Paint、Composite Layers 修改不同的CSS属性会触发不同阶段 触发的阶段越前,渲染的代价越高

2.网页就像搭积木:一旦积木位置移动-重排,上色-重绘

2.1.网页生成时,至少会渲染一次,用户访问过程中,还不会不断重新渲染(修改DOM、修改样式表、用户事件)

2.2.重绘不一定引起重排,但重排一定会引起重绘

2.3.重排发生原因:页面的初始化、引起的盒子变化、添加或者删除可见的DOM元素、元素位置改变、元素尺寸改变、元素内容改变(例如:一个文本被另一个不同尺寸的图片替代)、页面渲染初始化(无法避免)、浏览器窗口尺寸改变、读取CSS相关属性也会触发重排 

图片 14

2.3.1.尽量不触发Layout、使用transform代替top,left的动画

2.4.重绘:外观改变:当修改border-radius,box-shadow,color,backgroud等展示相关属性时候,会触发重绘、在经常paint的区域,要避免代价太高style、、不要用gif图,或者在不需要时,display:none,减少绘制区域,为引起大范围Paint的元素生成独立的Layer(比如将动画部分设置position:absolute)

setTimeout(有延时差)

1.setTimeout不够精确,它依靠浏览器内置时钟更新频率,不同浏览器更新频率不同

    1.1:setTimeout(fn, 1/60);

    1.2:IE8及以前更新间隔为15.6ms,setTimeout 16.7需要两个15.6ms才触发,超过14.5ms就会丢帧

2.setTimeout的回调会加入到异步队列,需要等到主队列执行完,才会执行异步队列,所以不能时间保证做每一帧。

3.requestAnimationFrame

    定义绘制每一帧前的准备工作 requestAnimation(callback);

自动调节频率,callback工作太多无法在一帧内完成,会自动降低为30FPS,虽然频率降低但比丢帧好

Layer模型层

图片 15

* 浏览器根据CSS属性为元素生成Layers

* 将Layers作为位图上传到GPU

* 当改变Layer的transform,opcity等属性时,渲染会跳过Layout,paint,直接通知GPU对Layer做变换

Layer层创建标准

根元素、拥有3dtransform属性、使用animation,transition实现 opacity,transform的动画

position、transform、半透明、CSS滤镜fitters、Canvas2D、video、溢出,Flash,

z-index大于某个相邻节点的Layer的元素

HTML的渲染机制

图片 16

图片 17

图片 18

图片 19

图片 20

图片 21

TAG标签: css 程序员
版权声明:本文由彩民之家高手论坛发布于前端知识,转载请注明出处:css渲染流程及动画深入理解和优化方案