Si vous devez utiliser setTimeout ou setInterval pour implémenter l'animation, la raison ne peut être que que vous avez besoin d'un contrôle précis de l'animation. Mais je pense qu'au moins à l'heure actuelle, les navigateurs avancés et même les navigateurs mobiles sont suffisamment populaires pour vous donner une raison d'utiliser des méthodes plus efficaces lors de la mise en œuvre d'animations.
La page est dessinée par le système (GPU ou CPU) à chaque changement de frame. Mais ce type de dessin est différent du dessin des jeux PC. Sa fréquence de dessin maximale est limitée par la fréquence de rafraîchissement du moniteur (et non de la carte graphique), donc dans la plupart des cas, la fréquence de dessin la plus élevée ne peut être que de 60 images par seconde ( image par seconde). (ci-après dénommé fps), correspondant à 60 Hz de l’affichage. 60 ips est l'état le plus idéal. Dans les tests quotidiens de performances des pages, 60 ips est également un indicateur important, plus c'est proche, mieux c'est. Parmi les outils de débogage de Chrome, il existe de nombreux outils utilisés pour mesurer le numéro de frame actuel :
Dans le prochain travail, nous utiliserons ces outils pour voir les performances de nos pages dans temps réel.
60 images par seconde, c'est à la fois de la motivation et de la pression car cela signifie que nous n'avons que 16,7 millisecondes (1000/60) pour dessiner chaque image. Si vous utilisez setTimeout ou setInterval (ci-après collectivement appelés timer) pour contrôler le dessin, des problèmes surviennent.
Tout d’abord, le calcul du délai du Timer n’est pas assez précis. Le calcul du délai repose sur l'horloge intégrée du navigateur et la précision de l'horloge dépend de la fréquence des mises à jour de l'horloge (résolution du minuteur). L'intervalle de mise à jour pour IE8 et les versions précédentes d'IE est de 15,6 millisecondes. Supposons que le délai setTimeout que vous définissez est de 16,7 ms, il doit alors être mis à jour deux fois de 15,6 millisecondes avant que le délai ne soit déclenché. Cela signifie également un retard inexpliqué de 15,6 x 2 – 16,7 = 14,5 millisecondes.
16.7ms DELAY: |------------| CLOCK: |----------|----------| 15.6ms 15.6ms
Ainsi, même si vous définissez le délai de setTimeout sur 0 ms, il ne se déclenchera pas immédiatement. À l'heure actuelle, la fréquence de mise à jour des navigateurs Chrome et IE9 est de 4 ms (si vous utilisez un ordinateur portable et utilisez la batterie au lieu du mode d'alimentation, afin d'économiser des ressources, le navigateur passera la fréquence de mise à jour à la même heure système, c'est-à-dire signifie des mises à jour moins fréquentes).
En prenant du recul, si la résolution de la minuterie peut atteindre 16,7 ms, elle sera également confrontée à un problème de file d'attente asynchrone. En raison de la relation asynchrone, la fonction de rappel dans setTimeout n'est pas exécutée immédiatement, mais doit être ajoutée à la file d'attente. Mais le problème est que s'il y a un nouveau script de synchronisation qui doit être exécuté en attendant le déclencheur retardé, le script de synchronisation ne sera pas mis en file d'attente après le rappel du timer, mais sera exécuté immédiatement, comme le code suivant :
function runForSeconds(s) { var start = +new Date(); while (start + s * 1000 > (+new Date())) {} } document.body.addEventListener("click", function () { runForSeconds(10); }, false); setTimeout(function () { console.log("Done!"); }, 1000 * 3);
Si quelqu'un clique sur le corps en attendant les 3 secondes pour déclencher le délai, le rappel sera-t-il toujours déclenché à temps lorsque les 3 secondes seront écoulées ? Bien sûr que non, cela attendra 10 secondes. Les fonctions synchrones ont toujours priorité sur les fonctions asynchrones :
等待3秒延迟 | 1s | 2s | 3s |--->console.log("Done!"); 经过2秒 |----1s----|----2s----| |--->console.log("Done!"); 点击body后 以为是这样:|----1s----|----2s----|----3s----|--->console.log("Done!")--->|------------------10s----------------| 其实是这样:|----1s----|----2s----|------------------10s----------------|--->console.log("Done!");
John Resign a trois articles sur les performances et la précision du minuteur : 1. Précision du temps JavaScript, 2. Analyse du minuteur. Performances, 3.Comment fonctionnent les minuteries JavaScript. Dans l'article, vous pouvez constater certains problèmes avec Timer sous différentes plates-formes, navigateurs et systèmes d'exploitation.
En prenant du recul, en supposant que la résolution du timer puisse atteindre 16,7 ms, et en supposant que la fonction asynchrone ne sera pas retardée, l'animation contrôlée par le timer est toujours insatisfaisante. C’est ce dont parlera la section suivante.
Veuillez me permettre d'introduire une autre constante 60 - le taux de rafraîchissement de l'écran 60Hz.
Quelle est la relation entre 60 Hz et 60 ips ? Il n'y a aucune relation. fps représente la fréquence à laquelle le GPU restitue l'image et Hz représente la fréquence à laquelle le moniteur rafraîchit l'écran. Pour une image statique, vous pouvez dire que le fps de cette image est de 0 image/seconde, mais vous ne pouvez jamais dire que le taux de rafraîchissement de l'écran à ce moment est de 0 Hz, ce qui signifie que le taux de rafraîchissement ne change pas avec le changement. du contenu de l’image. Qu’il s’agisse d’un jeu ou d’un navigateur, lorsque nous parlons de chutes d’images, cela signifie que le GPU restitue l’image moins fréquemment. Par exemple, elle descend à 30fps voire 20fps, mais du fait du principe de persistance de la vision, l'image que l'on voit est toujours mouvante et cohérente.
Dans la continuité de la section précédente, nous supposons que chaque minuterie ne sera pas retardée, ni interférée par les fonctions de synchronisation, et peut même raccourcir le temps à 16 ms, alors que se passera-t-il :
(Cliquez sur l'image pour l'agrandir)
La perte d'image s'est produite à 22 secondes
Si le délai est raccourci, le nombre d'images perdues sera Plus :
La situation réelle sera bien plus compliquée qu'on ne l'imaginait ci-dessus. Même si vous pouvez accorder un délai fixe pour résoudre le problème de perte d'image sur un écran à 60 Hz, que devez-vous faire avec des moniteurs avec d'autres taux de rafraîchissement. Vous devez savoir que différents appareils, ou même le même appareil, ont des taux de rafraîchissement d'écran différents ? différentes conditions de batterie.
以上同时还忽略了屏幕刷新画面的时间成本。问题产生于GPU渲染画面的频率和屏幕刷新频率的不一致:如果GPU渲染出一帧画面的时间比显示器刷新一张画面的时间要短(更快),那么当显示器还没有刷新完一张图片时,GPU渲染出的另一张图片已经送达并覆盖了前一张,导致屏幕上画面的撕裂,也就是是上半部分是前一张图片,下半部分是后一张图片:
PC游戏中解决这个问题的方法是开启垂直同步(v-sync),也就是让GPU妥协,GPU渲染图片必须在屏幕两次刷新之间,且必须等待屏幕发出的垂直同步信号。但这样同样也是要付出代价的:降低了GPU的输出频率,也就降低了画面的帧数。以至于你在玩需要高帧数运行的游戏时(比如竞速、第一人称射击)感觉到“顿卡”,因为掉帧。
在这里不谈requestAnimationFrame(以下简称rAF)用法,具体请参考MDN:Window.requestAnimationFrame()。我们来具体谈谈rAF所解决的问题。
从上一节我们可以总结出实现平滑动画的两个因素
时机(Frame Timing): 新的一帧准备好的时机
成本(Frame Budget): 渲染新的一帧需要多长的时间
这个Native API把我们从纠结于多久刷新的一次的困境中解救出来(其实rAF也不关心距离下次屏幕刷新页面还需要多久)。当我们调用这个函数的时候,我们告诉它需要做两件事: 1. 我们需要新的一帧;2.当你渲染新的一帧时需要执行我传给你的回调函数
那么它解决了我们上面描述的第一个问题,产生新的一帧的时机。
那么第二个问题呢。不,它无能为力。比如可以对比下面两个页面:
DEMO
DEMO-FIXED
对比两个页面的源码,你会发现只有一处不同:
// animation loop function update(timestamp) { for(var m = 0; m < movers.length; m++) { // DEMO 版本 //movers[m].style.left = ((Math.sin(movers[m].offsetTop + timestamp/1000)+1) * 500) + 'px'; // FIXED 版本 movers[m].style.left = ((Math.sin(m + timestamp/1000)+1) * 500) + 'px'; } rAF(update); }; rAF(update);
DEMO版本之所以慢的原因是,在修改每一个物体的left值时,会请求这个物体的offsetTop值。这是一个非常耗时的reflow操作(具体还有哪些耗时的reflow操作可以参考这篇: How (not) to trigger a layout in WebKit)。这一点从Chrome调试工具中可以看出来(截图中的某些功能需要在Chrome canary版本中才可启用)
未矫正的版本
可见大部分时间都花在了rendering上,而矫正之后的版本:
rendering时间大大减少了
但如果你的回调函数耗时真的很严重,rAF还是可以为你做一些什么的。比如当它发现无法维持60fps的频率时,它会把频率降低到30fps,至少能够保持帧数的稳定,保持动画的连贯。
没有什么是万能的,面对上面的情况,我们需要对代码进行组织和优化。
看看下面这样一段代码:
function jank(second) { var start = +new Date(); while (start + second * 1000 > (+new Date())) {} } p.style.backgroundColor = "red"; // some long run task jank(5); p.style.backgroundColor = "blue";
无论在任何的浏览器中运行上面的代码,你都不会看到p变为红色,页面通常会在假死5秒,然后容器变为蓝色。这是因为浏览器的始终只有一个线程在运行(可以这么理解,因为js引擎与UI引擎互斥)。虽然你告诉浏览器此时p背景颜色应该为红色,但是它此时还在执行脚本,无法调用UI线程。
有了这个前提,我们接下来看这段代码:
var p = document.getElementById("foo"); var currentWidth = p.innerWidth; p.style.backgroundColor = "blue"; // do some "long running" task, like sorting data
这个时候我们不仅仅需要更新背景颜色,还需要获取容器的宽度。可以想象它的执行顺序如下:
当我们请求innerWidth一类的属性时,浏览器会以为我们马上需要,于是它会立即更新容器的样式(通常浏览器会攒着一批,等待时机一次性的repaint,以便节省性能),并把计算的结果告诉我们。这通常是性能消耗量大的工作。
但如果我们并非立即需要得到结果呢?
上面的代码有两处不足,
更新背景颜色的代码过于提前,根据前一个例子,我们知道,即使在这里告知了浏览器我需要更新背景颜色,浏览器至少也要等到js运行完毕才能调用UI线程;
假设后面部分的long runing代码会启动一些异步代码,比如setTimeout或者Ajax请求又或者web-worker,那应该尽早为妙。
综上所述,如果我们不是那么迫切的需要知道innerWidth,我们可以使用rAF推迟这部分代码的发生:
requestAnimationFrame(function(){ var el = document.getElementById("foo"); var currentWidth = el.innerWidth; el.style.backgroundColor = "blue"; // ... }); // do some "long running" task, like sorting data
可见即使我们在这里没有使用到动画,但仍然可以使用rAF优化我们的代码。执行的顺序会变成:
在这里rAF的用法变成了:把代码推迟到下一帧执行。
有时候我们需要把代码推迟的更远,比如这个样子:
再比如我们想要一个效果分两步执行:1.p的display变为block;2. p的top值缩短移动到某处。如果这两项操作都放入同一帧中的话,浏览器会同时把这两项更改应用于容器,在同一帧内。于是我们需要两帧把这两项操作区分开来:
requestAnimationFrame(function(){ el.style.display = "block"; requestAnimationFrame(function(){ // fire off a CSS transition on its `top` property el.style.top = "300px"; }); });
这样的写法好像有些不太讲究,Kyle Simpson有一个开源项目h5ive,它把上面的用法封装了起来,并且提供了API。实现起来非常简单,摘一段代码瞧瞧:
function qID(){ var id; do { id = Math.floor(Math.random() * 1E9); } while (id in q_ids); return id; } function queue(cb) { var qid = qID(); q_ids[qid] = rAF(function(){ delete q_ids[qid]; cb.apply(publicAPI,arguments); }); return qid; } function queueAfter(cb) { var qid; qid = queue(function(){ // do our own rAF call here because we want to re-use the same `qid` for both frames q_ids[qid] = rAF(function(){ delete q_ids[qid]; cb.apply(publicAPI,arguments); }); }); return qid; }
使用方法:
// 插入下一帧 id1 = aFrame.queue(function(){ text = document.createTextNode("##"); body.appendChild(text); }); // 插入下下一帧 id2 = aFrame.queueAfter(function(){ text = document.createTextNode("!!"); body.appendChild(text); });
先从一个2011年twitter遇到的bug说起。
当时twitter加入了一个新功能:“无限滚动”。也就是当页面滚至底部的时候,去加载更多的twitter:
$(window).bind('scroll', function () { if (nearBottomOfPage()) { // load more tweets ... } });
但是在这个功能上线之后,发现了一个严重的bug:经过几次滚动到最底部之后,滚动就会变得奇慢无比。
经过排查发现,原来是一条语句引起的:$details.find(“.details-pane-outer”);
这还不是真正的罪魁祸首,真正的原因是因为他们将使用的jQuery类库从1.4.2升级到了1.4.4版。而这jQuery其中一个重要的升级是把Sizzle的上下文选择器全部替换为了querySelectorAll。但是这个接口原实现使用的是getElementsByClassName。虽然querySelectorAll在大部分情况下性能还是不错的。但在通过Class名称选择元素这一项是占了下风。有两个对比测试可以看出来:1.querySelectorAll v getElementsByClassName 2.jQuery Simple Selector
通过这个bug,John Resig给出了一条(实际上是两条,但是今天只取与我们话题有关的)非常重要的建议
It’s a very, very, bad idea to attach handlers to the window scroll event.
他想表达的意思是,像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.php.cn/ 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();
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去实现呢?
Je ne suis pas un employé de Taobao, mais ma première hypothèse est que la raison de cela est d'utiliser le hack Translate3d. En termes simples, si vous ajoutez l'attribut -webkit-transform:translateZ(0); ou -webkit-transform:transform3d(0,0,0); éléments., améliorant la vitesse et les performances par rapport au rendu CPU classique. (Je suis presque sûr que cela permettra l'accélération matérielle dans Chrome, mais il n'y a aucune garantie sur d'autres plates-formes. D'après les informations que j'ai obtenues, cela est également applicable dans la plupart des navigateurs tels que Firefox et Safari).
Mais cette affirmation n'est en réalité pas exacte, du moins dans la version actuelle de Chrome, ce n'est pas un hack. Car par défaut toutes les pages web passeront par le GPU lors du rendu. Alors est-ce encore nécessaire ? avoir. Avant de comprendre le principe, il faut d’abord comprendre la notion de calque.
html sera converti en arborescence DOM dans le navigateur, et chaque nœud de l'arborescence DOM sera converti en un RenderObject Plusieurs RenderObjects peuvent correspondre à un ou plusieurs RenderLayer. Le processus de rendu du navigateur est le suivant :
Récupérez le DOM et divisez-le en plusieurs couches (RenderLayer)
Grille chaque couche, et dessinez les bitmaps indépendamment
Téléchargez ces bitmaps sur le GPU sous forme de textures
Composez plusieurs calques pour générer l'image d'écran finale (couche ultime).
Ceci est similaire au rendu 3D du jeu. Bien que nous voyons un personnage en trois dimensions, la peau de ce personnage est « collée » et « reconstituée » à partir de différentes images. de. La page Web comporte une étape supplémentaire et, bien que la page Web finale soit composée de plusieurs couches bitmap, ce que nous voyons n'est qu'une copie et, en fin de compte, il n'y a qu'une seule couche. Bien entendu, certaines couches ne peuvent pas être combinées, comme le flash. En prenant comme exemple une page de lecture d'iQiyi (http://www.php.cn/), nous pouvons utiliser le panneau Calque de Chrome (non activé par défaut et doit être activé manuellement) pour afficher tous les calques de la page :
On voit que la page est composée des couches suivantes :
OK, alors voici la question.
Supposons que je veuille maintenant changer le style d'un conteneur (peut être vu comme une étape d'animation), et dans le pire des cas, changer sa longueur et sa largeur - pourquoi est-il préférable de changer la longueur et la largeur ? Quelle mauvaise situation. Habituellement, changer le style d'un objet nécessite les quatre étapes suivantes :
Toute modification d'attribut entraînera le navigateur à recalculer le style du conteneur, par exemple si vous modifiez la taille du conteneur ou de la position (redistribution), alors la première chose affectée est la taille et la position du conteneur (cela affecte également la position des nœuds adjacents de son nœud parent associé, etc.), puis le navigateur doit repeindre le conteneur ; mais Si vous modifiez uniquement la couleur d'arrière-plan du conteneur et d'autres propriétés qui n'ont rien à voir avec la taille du conteneur, vous gagnez du temps lors du calcul de la position lors de la première étape. En d’autres termes, si les changements d’attributs commencent plus tôt (plus haut) dans le graphique en cascade, l’impact sera plus grand et l’efficacité sera moindre. La redistribution et la repeinture entraîneront le redessinage des bitmaps de la couche où se trouvent tous les nœuds concernés, et le processus ci-dessus sera exécuté à plusieurs reprises, ce qui entraînera une efficacité réduite.
Afin de minimiser le coût, il est bien entendu préférable de ne laisser que l'étape de la couche de composition. Supposons que lorsque l'on change le style d'un conteneur, cela n'affecte que lui-même, et il n'est pas nécessaire de le redessiner. Ne serait-il pas préférable de changer le style directement en changeant les attributs de la texture dans le GPU ? Ceci est bien sûr réalisable, à condition d'avoir votre propre calque
C'est aussi le principe du hack d'accélération matérielle ci-dessus, et aussi le principe de l'animation CSS - créez votre propre calque pour l'élément, plutôt qu'avec la plupart des éléments de la page Les éléments partagent un calque.
Quels types d'éléments peuvent créer son propre calque ? Dans Chrome, au moins une des conditions suivantes doit être remplie :
Le calque possède des propriétés CSS de transformation 3D ou de perspective (propriétés avec des éléments 3D)
Le calque est utilisé par l'élément
Le calque est utilisé par un élément
La couche est utilisée pour un plugin composite (plug-in, tel que Flash)
Le calque utilise une animation CSS pour son opacité ou utilise une transformation webkit animée (animation CSS)
Le calque utilise des filtres CSS accélérés (filtre CSS)
Le calque avec un descendant composé contient des informations qui doivent figurer dans l'arborescence des calques composites, telles qu'un clip ou une réflexion (il existe un élément descendant qui est un calque indépendant)
Couche a un frère avec un z-index inférieur qui a un calque de composition (en d'autres termes, le calque est rendu au-dessus d'un calque composite) (les éléments adjacents d'un élément sont des calques indépendants)
Évidemment, ce que nous venons de voir. Le flash dans la page de lecture et l'image de focus avec le style Translate3D activé remplissent les conditions ci-dessus.
同时你也可以勾选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时出现的问题。
以上就是Javascript高性能动画与页面渲染的内容,更多相关内容请关注PHP中文网(www.php.cn)!