视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
Javascript高性能动画与页面渲染
2020-11-27 20:24:48 责编:小采
文档

他想表达的意思是,像scroll,resize这一类的事件会非常频繁的触发,如果把太多的代码放进这一类的回调函数中,会延迟页面的滚动,甚至造成无法响应。所以应该把这一类代码分离出来,放在一个timer中,有间隔的去检查是否滚动,再做适当的处理。比如如下代码:

var didScroll = false;

$(window).scroll(function() {
 didScroll = true;
});

setInterval(function() {
 if ( didScroll ) {
 didScroll = false;
 // Check your page position and then
 // Load in more results
 }
}, 250)

这样的作法类似于Nicholas将需要长时间运算的循环分解为“片”来进行运算:

// 具体可以参考他写的《javascript高级程序设计》
// 也可以参考他的这篇博客: http://www.gxlcms.com/
function chunk(array, process, context){
 var items = array.concat(); //clone the array
 setTimeout(function(){
 var item = items.shift();
 process.call(context, item);

 if (items.length > 0){
 setTimeout(arguments.callee, 100);
 }
 }, 100);
}

原理其实是一样的,为了优化性能、为了防止浏览器假死,将需要长时间运行的代码分解为小段执行,能够使浏览器有时间响应其他的请求。

回到rAF上来,其实rAF也可以完成相同的功能。比如最初的滚动代码是这样:

function onScroll() {
 update();
}

function update() {

 // assume domElements has been declared
 for(var i = 0; i < domElements.length; i++) {

 // read offset of DOM elements
 // to determine visibility - a reflow

 // then apply some CSS classes
 // to the visible items - a repaint

 }
}

window.addEventListener('scroll', onScroll, false);

这是很典型的反例:每一次滚动都需要遍历所有元素,而且每一次遍历都会引起reflow和repaint。接下来我们要做的事情就是把这些费时的代码从update中解耦出来。

首先我们仍然需要给scroll事件添加回调函数,用于记录滚动的情况,以方便其他函数的查询:

var latestKnownScrollY = 0;

function onScroll() {
 latestKnownScrollY = window.scrollY;
}

接下来把分离出来的repaint或者reflow操作全部放入一个update函数中,并且使用rAF进行调用:

function update() {
 requestAnimationFrame(update);

 var currentScrollY = latestKnownScrollY;

 // read offset of DOM elements
 // and compare to the currentScrollY value
 // then apply some CSS classes
 // to the visible items
}

// kick off
requestAnimationFrame(update);

其实解耦的目的已经达到了,但还需要做一些优化,比如不能让update无限执行下去,需要设标志位来控制它的执行:

var latestKnownScrollY = 0,
 ticking = false;

function onScroll() {
 latestKnownScrollY = window.scrollY;
 requestTick();
} 

function requestTick() {
 if(!ticking) {
 requestAnimationFrame(update);
 }
 ticking = true;
}

并且我们始终只需要一个rAF实例的存在,也不允许无限次的update下去,于是我们还需要一个出口:

function update() {
 // reset the tick so we can
 // capture the next onScroll
 ticking = false;

 var currentScrollY = latestKnownScrollY;

 // read offset of DOM elements
 // and compare to the currentScrollY value
 // then apply some CSS classes
 // to the visible items
}

// kick off - no longer needed! Woo.
// update();

理解Layer

Kyle Simpson说:

Rule of thumb: don’t do in JS what you can do in CSS.

如以上所说,即使使用rAF,还是会有诸多的不便。我们还有一个选择是使用css动画:虽然浏览器中UI线程与js线程是互斥,但这一点对css动画不成立。

在这里不聊css动画的用法。css动画运用的是什么原理来提升浏览器性能的。

首先我们看看淘宝首页的焦点图:

我想提出一个问题,为什么明明可以使用translate 2d去实现的动画,它要用3d去实现呢?

我不是淘宝的员工,但我的第一猜测这么做的原因是为了使用translate3d hack。简单来说如果你给一个元素添加上了-webkit-transform: translateZ(0);或者-webkit-transform: translate3d(0,0,0);属性,那么你就等于告诉了浏览器用GPU来渲染该层,与一般的CPU渲染相比,提升了速度和性能。(我很确定这么做会在Chrome中启用了硬件加速,但在其他平台不做保证。就我得到的资料而言,在大多数浏览器比如Firefox、Safari也是适用的)。

但这样的说法其实并不准确,至少在现在的Chrome版本中这算不上一个hack。因为默认渲染所有的网页时都会经过GPU。那么这么做还有必要吗?有。在理解原理之前,你必须先了解一个层(Layer)的概念。

html在浏览器中会被转化为DOM树,DOM树的每一个节点都会转化为RenderObject, 多个RenderObject可能又会对应一个或多个RenderLayer。浏览器渲染的流程如下:

  1. 获取 DOM 并将其分割为多个层(RenderLayer)

  2. 将每个层栅格化,并的绘制进位图中

  3. 将这些位图作为纹理上传至 GPU

  4. 复合多个层来生成最终的屏幕图像(终极layer)。

这和游戏中的3D渲染类似,虽然我们看到的是一个立体的人物,但这个人物的皮肤是由不同的图片“贴”和“拼”上去的。网页比此还多了一个步骤,虽然最终的网页是由多个位图层合成的,但我们看到的只是一个复印版,最终只有一个层。当然有的层是无法拼合的,比如flash。以爱奇艺的一个播放页(http://www.gxlcms.com/)为例,我们可以利用Chrome的Layer面板(默认不启用,需要手动开启)查看页面上所有的层:

我们可以看到页面上由如下层组成:

OK,那么问题来了。

假设我现在想改变一个容器的样式(可以看做动画的一个步骤),并且是一种最糟糕的情况,改变它的长和宽——为什么说改变长和宽是最糟糕的情况呢。通常改变一个物体的样式需要以下四个步骤:

任何属性的改变都导致浏览器重新计算容器的样式,比如你改变的是容器的尺寸或者位置(reflow),那么首先影响的就是容器的尺寸和位置(也影响了与它相关的父节点自己点相邻节点的位置等),接下来浏览器还需要对容器重新绘制(repaint);但如果你改变的只是容器的背景颜色等无关容器尺寸的属性,那么便省去了第一步计算位置的时间。也就是说如果改变属性在瀑布图中开始的越早(越往上),那么影响就越大,效率就越低。reflow和repaint会导致所有受影响节点所在layer的位图重绘,反复执行上面的过程,导致效率降低。

为了把代价降到最低,当然最好只留下compositing layer这一个步骤即可。假设当我们改变一个容器的样式时,影响的只是它自己,并且还无需重绘,直接通过在GPU中改变纹理的属性来改变样式,岂不是更好?这当然是可以实现的,前提是你有自己的layer

这也是上面硬件加速hack的原理,也是css动画的原理——给元素创建自己layer,而非与页面上大部分的元素共用layer。

什么样的元素才能创建自己layer呢?在Chrome中至少要符合以下条件之一:

  • Layer has 3D or perspective transform CSS properties(有3D元素的属性)

  • Layer is used by <video> element using accelerated video decoding(video标签并使用加速视频解码)

  • Layer is used by a <canvas> element with a 3D context or accelerated 2D context(canvas元素并启用3D)

  • Layer is used for a composited plugin(插件,比如flash)

  • Layer uses a CSS animation for its opacity or uses an animated webkit transform(CSS动画)

  • Layer uses accelerated CSS filters(CSS滤镜)

  • Layer with a composited descendant has information that needs to be in the composited layer tree, such as a clip or reflection(有一个后代元素是的layer)

  • Layer has a sibling with a lower z-index which has a compositing layer (in other words the layer is rendered on top of a composited layer)(元素的相邻元素是layer)

  • 很明显刚刚我们看到的播放页中的flash和开启了translate3d样式的焦点图符合上面的条件。

    同时你也可以勾选Chrome开发工具中的rendering选显卡下的Show composited layer borders 选项。页面上的layer便会加以边框区别开来。为了验证我们的想法,看下面这样一段代码:

    <html>
    <head>
     <style type="text/css">
     p {
     -webkit-animation-duration: 5s;
     -webkit-animation-name: slide;
     -webkit-animation-iteration-count: infinite;
     -webkit-animation-direction: alternate;
     width: 200px;
     height: 200px;
     margin: 100px;
     background-color: skyblue;
     }
     @-webkit-keyframes slide {
     from {
     -webkit-transform: rotate(0deg);
     }
     to {
     -webkit-transform: rotate(120deg);
     }
     }
     </style>
    </head>
    <body>
     <p id="foo">I am a strange root.</p>
    </body>
    </html>

    运行时的timeline截图如下:

    可见元素有自己的layer,并且在动画的过程中没有触发reflow和repaint。

    最后再看看淘宝首页,不仅仅只有焦点图才拥有了的layer:

    但太多的layer也未必是一件好事情,有兴趣的同学可以看一看这篇文章:Jank Busting Apple’s Home Page。看一看在苹果首页太多layer时出现的问题。

    下载本文
    显示全文
    专题