Comment avoir un rendu performant même sur mobile ? Comment animer convenablement ses interfaces Web ? Quelques pistes et retours d'expérience.
Présenté en 2015 pour le JS Open days 1
3. Pour faire plaisir à l’utilisateur (et ton
CA)
Temps de réponse :
• < 150 ms : instantané
– Le but de toute interface touch
• 150 ms — 1 s : c’est la machine qui bosse
– L’utilisateur stresse
• > 10s : bye bye
Animations :
• 60 Fps : fluide et naturel
• < 30 Fps : bye bye
9. Les ordres de grandeur
60 Hz = 16 ms pour :
• JS
– Récupérer la position de l’élément
– Calcul de la nouvelle position
– Appliquer la nouvelle position
• Navigateur
– Calculs de Layout
– Paint des zones modifées
– Pousser vers la carte graphique
10. Les manipulations DOM en JS
• el.offsetLeft …, el.clientLeft…, el.
getBoundingClientRect()…
• el.scrollTo(), el.scrollTop, w.innerHeight
• el.getComputedStyle()
• evt.layerX, evt.offsetX
• SVG.getCharNumAtPosition(),
SVG.getNumberOfChars()
jQuery.animate() utilise tout cela !
11. Animation prédictible ? JS quasi inutile
• On trash jQuery.animate
• Pré-calcul des styles
• JS se limite à ajouter / supprimer des classes
El1.addClass(‘disappear-to-left’)
El2.addClass(‘appear-from-right’)
13. Transitions CSS
• JS :
– Récupérer la position de l’élément
– Calcul de la nouvelle position
– Appliquer la nouvelle position
• Navigateur
– Calculs de Layout
– Paint des zones modifées
– Pousser vers la carte graphique
14. Les propriétés qui coûtent cher
Elles déclenchent Layout + Paint + Composite
• top, left, width, height, float
• font-*, border-*, padding-*, margin-*
• display, visibility
csstriggers.com
15. Les propriétés qui vont bien
.move-left{
transform: translateX(-600px);
}
.enlarge-your p {
transform : scale(1.3, 0) ;
}
• IE 8 : ms-filters, IE9 : prefix ms-
• IE10, chrome, Fx, OS mobile : standard
16. CSS transform / opacity
• JS :
– Récupérer la position de l’élément
– Calcul de la nouvelle position
– Appliquer la nouvelle position
• Navigateur
– Calculs de Layout
– Paint des zones modifées
– Pousser vers la carte graphique
17. Bonus : forcer le GPU (parfois)
.move-left {
transform:translate3d(-600px, 0, 0);
}
.enlarge-your p {
transform:scale3d(1.3, 0, 0);
}
19. Transition + translation + GPU = <3
• JS :
– Récupérer la position de l’élément
– Calcul de la nouvelle position
– Appliquer la nouvelle position
• Navigateur
– Calculs de Layout
– Paint des zones modifées
– Pousser vers la carte graphique
22. Et pour les animations imprévisibles ?
• Parallax
• Drag
• Jeux vidéos
• …
23. Reprenons
• JS :
– Récupérer la position de l’élément
– Calcul de la nouvelle position
– Appliquer la nouvelle position
• Navigateur
– Calculs de Layout <= optimisé en CSS
– Paint des zones modifées <= optimisé en CSS
– Pousser vers la carte graphique
24. Récupérer la position : mise en cache
• Plutôt que
$(el).on(‘touchmove’, function move() {
// get + set du DOM en boucle : BOOM
this.style.width = (this.offsetWidth + X);
}
• Préférer
var width = el.offsetWidth;
$(el).on(‘touchmove’, function move() {
// un set, et même plusieurs d’affilé : SMOOTH
this.style.width = ( width += Y );
}
25. Reprenons
• JS :
– Récupérer la position de l’élément
– Calcul de la nouvelle position
– Appliquer la nouvelle position
• Navigateur
– Calculs de Layout
– Paint des zones modifées
– Pousser vers la carte graphique
26. Quand mettre à jour le DOM ?
// JAMAIS
setInterval( move, 16 );
// MIEUX, mais agressif et imprécis
(function boucle() {
setTimeout( boucle, 16);
move();
}());
// AU TOP
(function boucle() {
requestAnimationFrame(
boucle);
move();
}());
27. Alléger
var height = el.offsetHeight,
Y = 0;
$(el).on(‘touchmove’, function calculateDelta() {
… // Calcul séparé de Y (delta du doigt)
});
requestAnimationFrame( function move() {
… // check de la nécessité puis Raf(move)
this.style.height = (height += Y);
});
28. Reprenons
• JS :
– Récupérer la position de l’élément
– Calcul de la nouvelle position <= dissocié
– Appliquer la nouvelle position <= optimisé
• Navigateur
– Calculs de Layout
– Paint des zones modifées
– Pousser vers la carte graphique
29. Calculs lourds ?
var IA = new Worker(‘game-ia.js’);
var positions;
IA.addEventListener(‘moves’,function(e){
positions = e.data;
});
(function boucle() {
requestAnimationFrame( boucle );
moveUnits( positions );
})();
30. Calculs lourds ?
La technique ancestrale du
setTimeout( fn, 0);
Universel, increvable, lisible avec le bon snippet
31. Reprenons
• JS :
– Récupérer la position de l’élément
– Calcul de la nouvelle position <= dissocié et optimisé
– Appliquer la nouvelle position <= optimisé
• Navigateur
– Calculs de Layout
– Paint des zones modifées
– Pousser vers la carte graphique
32. DOM : la taille compte
(et la manière dont on s’en sert)
DOM Monster yellowlab.tools
33. Touche pas (trop) à mon DOM
• Peu de requête DOM
– Mise en cache des résultats en dehors des boucles
• Appliquer en batchs
– $el.addClass(‘error’) plutôt que $el.css
– .innerHTML marche encore !
– Pas d’alternance get / set
34. Apprends à faire tes sélecteurs
• Natif quand tu peux :
$(document.getElementById(‘id’)) VS $(‘#id’)
document.querySelector(‘.item’) VS
$(‘.item’).first()
• Limiter l’étendue de la recherche
$container.find(‘.item’)
$container.find(‘ > li.item’)
• Déléguer
$container.on( ‘click’, ‘li’, function(){} )
42. Conclusion
• JS seul est performant, merci de demander
• Watch your DOM !
• Use the CSS 3 / HTML5 force Luke
Je testerais, tu testeras,
nous testerons …
Plus rapide OK, mais à quel point ?
Nvidia et sa plateforme GRID (jeu dans le cloud) : > 150 ms
Facebook A/B testing sur la fréquence en Hertz en passant de en passant de 60 fps à 30 fps : scroll baisse de l’« engagement » (moins de post, moins d’ouverture de l’app …)
Rumeur et test de Google, mais cela montre l’idée de Google depuis toujours.
L’interface fait semblant de marcher même offline.
En cas de détection offline, penser à rajouter un petit message bien technique : votre connexion est faible
Ça passe par des transitions visuelles : si elles rament, c’est pire
Ebay et Youtube utilisent la technique du loader discret
Les applications utilisent la technique du faux layout
Un peu de moins de FPS = moins d’engagement
Comment amener notre milieu de gamme (Galaxy Note 1, 600€ il y a 3 ans) au niveau d’un haut de gamme
Carroussel : Left de 2 images
Get / set de position : demande au DOM, coûte cher.
Layout : dimensions, position
Paint : shadow, radius …
Le simple fait de demander va bypasser le cache des valeurs déjà set, et forcer un recalcul
Une liste : https://gist.github.com/paulirish/5d52fb081b3570c81e3a
Liste complémentaire en SVG : http://gent.ilcore.com/2011/03/how-not-to-trigger-layout-in-webkit.html
Manière plus compliquée : 1 classe pour tout le slider, mais il faut générer dynamiquement les styles
De toute façon les navigateurs batchent les modifications du DOM
Durée des transitions : rester sous la demie seconde, sinon ça rame même quand c’est fluide
Marche partout, sans préfixes
Modèle de boite : on touche un truc, tout se recalcule, parents compris
En plus width et height font parfois des arrondis de pixels == bords flous
translateX et Y pour left / right, margin, padding
Scale pour width / height
rotate, skew(X|Y), matrix(3d) …
Moins de CPU, plus de RAM
Certains mobiles ignorent l’option 3D pour se préserver
D’autres mettent translate normal directement dans le GPU
Utilisation de will-change : trop tôt pour être sur de ce que ça fait
À réserver aux petites zones
On a mieux séparé nos responsabilités entre la vue et le modèle pour une animation simple
Yelp on va bouger les dimensions de la photo, son opacité, bouger le layout, le tout en suivant uniquement le doigt
Get / set de position : demande au DOM, coûte cher.
Layout et Paint réglés grâce à transform + translate, scale, opacity
1 : lecture + écriture : allers retours DOM trop fréquents
Au passage, c’est le comportement par défaut de jQuery (logique pour une librairie car c’est le plus safe)
X = delta du doigt depuis le dernier mouvement
2 : et en plus le navigateur peut batcher les Set successifs (4 ms)
Y = delta du doigt depuis le début
Récupération le moins possible
Quand appliquer la nouvelle position ?
setInterval demande au navigateur de rappeler quoi qu’il arrive : si une action dure plus de 16 ms, les callbacks s’empilent jusqu’à ce que la callstack sature
setTimeout laisse au navigateur le temps de souffler (avec de grands chiffres), mais des frames sont ratées alors qu’elles auraient pu être honorées. Avec des petites chiffres ( 5 ms), on surconsomme le CPU.
RaF : Le navigateur te rappelle lorsqu’il se sent chaud. Note que le move() se fait APRÈS le RaF
Du coup on calcule la position du doigt pendant l’événement touch
Mais on n’applique le style que lorsque c’est nécessaire
Rajouter un petit check « if moving »
IA.postMessage();
iOS, Android 4.4, IE 11 mobile, Fx (s’émule avec setTimeout 0 )
MS avait proposé setImmediate, Google a dit non : non merci on préfère optimiser setTimeout 0
Calculs : séparé de l’animation, au touch ou via Web Workers / setTimeout
Nouvelle position : RaF
Yellow Lab tools
DOM Monster
5000 nœuds c’est beaucoup mais classique si on fait du RWD de bourrin
Les librairies de data-binding comme Knockout et Angular font de l’atomique, mais c’est patchable
React tente le mix entre changements atomiques et batchs grâce à JSX
Si c’est pas ton quotidien : investis dans les outils
Se branche en USB sur la machine
Connu car poussé par les évangélistes Google
Tutoriels et post de blog sont vite obsolète
Pas super clair
Il ne fait que Chrome, pas le navigateur Android
http://blogs.msdn.com/b/visualstudioalm/archive/2014/04/04/diagnosing-mobile-website-issues-on-windows-phone-8-1-with-visual-studio.aspx
Visual Studio 2013 + Windows Phone 8.1
Tout aussi poussé, mais meilleure ergonomie que l’outil chrome devtools