Fermer

avril 20, 2018

Conseils d'optimisation des performances JavaScript: un aperçu –


Dans ce post, il y a beaucoup de choses à couvrir à travers un paysage large et changeant énormément. C'est aussi un sujet qui couvre le favori de tous: le framework JS du mois ™.

Nous allons essayer de nous en tenir au mantra «Outils, pas à des règles» et de minimiser les mots à la mode de JS. Puisque nous ne serons pas en mesure de couvrir tout ce qui concerne la performance de JS dans un article de 2000 mots, assurez-vous de lire les références et de faire vos propres recherches après.

Mais avant de nous plonger dans les détails, apprenons question en répondant à ce qui suit: ce qui est considéré comme du JavaScript performant, et comment cela s'inscrit-il dans le cadre plus large des métriques de performance du Web?

Préparer le terrain

Tout d'abord, mettons les choses à l'écart: vous testez exclusivement sur votre appareil mobile, vous excluez plus de 50% de vos utilisateurs.

 Le nombre d'utilisateurs d'ordinateurs portables a dépassé le nombre d'utilisateurs d'ordinateurs de bureau en novembre 2016

Cette tendance ne fera que continuer de croître, car la passerelle privilégiée du marché émergent vers le Web est un appareil Android de moins de 100 $. L'ère de l'ordinateur de bureau en tant qu'appareil principal pour accéder à Internet est terminée, et le prochain milliard d'internautes visiteront vos sites principalement via un appareil mobile.

Les tests en mode appareil de Chrome DevTools ne remplacent pas les tests sur un vrai appareil. L'utilisation de la CPU et de la régulation réseau aide, mais c'est une bête fondamentalement différente. Test sur de vrais appareils.

Même si vous faites des tests sur de vrais appareils mobiles, vous le faites probablement sur votre nouveau téléphone phare de 600 $. Le truc c'est que ce n'est pas le périphérique que vos utilisateurs ont. Le périphérique médian est quelque chose comme un Moto G1 – un périphérique avec moins de 1 Go de RAM, et un très faible processeur et GPU.

Voyons comment ça se cumule quand analyse un bundle JS moyen

Ouch. Bien que cette image ne couvre que l'analyse et le temps de compilation du JS (plus sur cela plus tard) et non la performance générale, elle est fortement corrélée et peut être considérée comme un indicateur des performances générales de JS.

] c'est le World-Wide Web, pas le Web occidental riche ". Ainsi, votre cible pour les performances Web est un appareil dont le ~ 25x est plus lent que votre MacBook ou votre iPhone. Laisse faire ça un peu. Mais ça devient pire. Voyons ce que nous visons réellement

Qu'est-ce que le code JS performant?

Maintenant que nous connaissons notre plate-forme cible, nous pouvons répondre à la question suivante: quoi est performant JS

Bien qu'il n'y ait pas de classification absolue de ce qui définit le code performant, nous avons un modèle de performance centré sur l'utilisateur que nous pouvons utiliser comme référence: Le modèle RAIL . votre application répond à une action de l'utilisateur en moins de 100ms, l'utilisateur perçoit la réponse comme immédiate. Cela s'applique aux éléments pouvant être tappés, mais pas au défilement ou au déplacement.

Animate

Sur un moniteur 60 Hz, nous voulons cibler une constante de 60 images par seconde lors de l'animation et du défilement. Cela se traduit par environ 16 ms par trame. Sur ce budget de 16 ms, vous disposez de 8 à 10 ms pour faire tout le travail, le reste étant pris en charge par le navigateur interne et d'autres variances

Travail inactif

Si vous avez une tâche coûteuse et continue, assurez-vous pour le découper en morceaux plus petits pour permettre au thread principal de réagir aux entrées de l'utilisateur. Vous ne devriez pas avoir une tâche qui retarde l'entrée de l'utilisateur pendant plus de 50 ms.

Charger

Vous devriez cibler un chargement de page en moins de 1000 ms. Tout est fini, et vos utilisateurs commencent à devenir nerveux. C'est un objectif assez difficile à atteindre sur les appareils mobiles en ce qui concerne la page interactive, et non seulement l'avoir peint à l'écran et défilement. En pratique, c'est encore moins:

En pratique, viser la marque 5s-interactive. C'est ce que Chrome utilise dans son Lighthouse audit .

Maintenant que nous connaissons les métriques, regardons quelques-unes des statistiques :

  • 53% des visites sont abandonnées si un site mobile prend plus de trois secondes à charger
  • 1 personne sur 2 s'attend à ce qu'une page se charge en moins de 2 secondes
  • 77% des sites mobiles mettent plus de 10 secondes à charger sur les réseaux 3G
  • 19 secondes est le temps de chargement moyen pour les sites mobiles sur les réseaux 3G

Et un peu plus, grâce à Addy Osmani :

  • applications devenues interactives en 8 secondes sur ordinateur (en utilisant le câble) et 16 secondes sur mobile (Moto G4 sur 3G)
  • à la médiane, les développeurs ont expédié 410 Ko de JS gzippés pour leurs pages

Se sentant suffisamment frustré? Bien. Mettons-nous au travail et réparons le web. ✊

Le contexte est tout

Vous avez peut-être remarqué que le principal goulot d'étranglement est le temps qu'il faut pour charger votre site Web. Plus précisément, le téléchargement de JavaScript, l'analyse, la compilation et le temps d'exécution. Il n'y a aucun moyen de contourner cela, mais de charger moins de JavaScript et de charger plus intelligemment.

Mais qu'en est-il du travail réel que votre code fait en dehors de démarrer le site Web? Il doit y avoir des gains de performance, non?

Avant de vous lancer dans l'optimisation de votre code, réfléchissez à ce que vous construisez. Construisez-vous un framework ou une librairie VDOM? Votre code doit-il faire des milliers d'opérations par seconde? Est-ce que vous faites une bibliothèque de temps critique pour gérer les entrées et / ou les animations de l'utilisateur? Sinon, vous voudrez peut-être déplacer votre temps et votre énergie quelque part de plus percutant.

Ce n'est pas que l'écriture d'un code performant n'ait aucune importance, mais elle a généralement peu ou pas d'impact sur les choses. . Donc, avant d'entrer dans un argument Stack Overflow sur .map vs .forEach vs pour boucles en comparant les résultats de JSperf.com, assurez-vous de voir le forêt et pas seulement les arbres. 50k ops / s peut sembler 50x meilleur que 1k ops / s sur le papier, mais cela ne fera pas de différence dans la plupart des cas.

Analyser, compiler et exécuter

Fondamentalement, le problème de la plupart des JS non-performants n'est pas exécuté le code lui-même, mais toutes les étapes qui doivent être prises avant le code commence même à s'exécuter.

Nous parlons ici de niveaux d'abstraction. Le processeur de votre ordinateur exécute le code machine. La plupart du code que vous utilisez sur votre ordinateur est au format binaire compilé. (J'ai dit code plutôt que programmes compte tenu de toutes les applications Electron de nos jours.) Signification, toutes les abstractions au niveau OS, elle fonctionne nativement sur votre matériel, pas de préparation

JavaScript n'est pas pré-compilé. Il arrive (via un réseau relativement lent) en tant que code lisible dans votre navigateur qui est, à toutes fins utiles, le "OS" pour votre programme JS.

Ce code doit d'abord être analysé – c'est-à-dire lu et retourné dans une structure indexable par ordinateur qui peut être utilisée pour la compilation. Il est ensuite compilé en bytecode et enfin en code machine, avant qu'il ne puisse être exécuté par votre appareil / navigateur.

Une autre chose importante à signaler est que JavaScript est mono-thread, et fonctionne sur le navigateur fil principal. Cela signifie qu'un seul processus peut s'exécuter à la fois. Si votre chronologie de performance DevTools est remplie de pics jaunes, si vous exécutez votre CPU à 100%, vous aurez des images longues / supprimées, des défilements janky et toutes sortes d'autres choses désagréables.

Donc tout ce travail doit être fait avant que votre JS commence à travailler. L'analyse et la compilation prennent jusqu'à 50% du temps total d'exécution de JS dans le moteur V8 de Chrome.

Il y a deux choses à retirer de cette section:

  1. Bien que pas nécessairement linéaire, JS analyse le temps Taille. Moins vous expédiez de JS, mieux c'est.
  2. Chaque framework JS que vous utilisez (React, Vue, Angular, Preact …) est un autre niveau d'abstraction (sauf s'il est précompilé, comme Svelte ). Non seulement cela augmentera la taille de votre groupe, mais cela ralentira aussi votre code puisque vous ne parlez pas directement au navigateur.

Il y a des façons d'atténuer cela, comme utiliser les travailleurs de service pour faire des tâches en arrière-plan et sur un autre thread, en utilisant asm.js pour écrire du code qui est plus facilement compilé pour les instructions machine, mais c'est un tout autre sujet.

Ce que vous pouvez faire, cependant, c'est d'éviter d'utiliser des frameworks d'animation JS pour tout ce qui déclenche les peintures et les mises en page . Utilisez les bibliothèques uniquement quand il n'y a absolument aucun moyen d'implémenter l'animation en utilisant des transitions et animations CSS normales.

Même si elles utilisent des transitions CSS, des propriétés composées et requestAnimationFrame () elles fonctionnent toujours en JS, sur le fil principal. Ils ne font que marteler votre DOM avec des styles en ligne toutes les 16ms, puisqu'il n'y a pas grand-chose d'autre à faire. Vous devez vous assurer que tous vos JS seront exécutés en moins de 8ms par image afin de garder les animations lisses.

Les animations CSS et les transitions, en revanche, tournent sur le thread principal – sur le GPU, si Considérant que la plupart des animations s'exécutent pendant le chargement ou l'interaction de l'utilisateur, cela peut donner à vos applications web l'espace nécessaire pour respirer.

L'API Web est un jeu de fonctionnalités à venir qui vous permettra de faire des animations JS performantes sur le thread principal, mais pour l'instant, respectez les transitions CSS et les techniques comme FLIP .

Les tailles des bundles sont tout

Aujourd'hui, tout est une question de paquets. Fini le temps de Bower et de dizaines de balises avant la balise fermante

. Maintenant, il s'agit de npm install - quel que soit le nouveau jouet que vous trouvez sur NPM, les regroupant avec Webpack dans un énorme fichier JS unique de 1 Mo et en explorant le navigateur de vos utilisateurs à une exploration tout en plafonnant leurs plans de données. Essayez d'expédier moins de JS. Vous n'avez peut-être pas besoin de toute la bibliothèque Lodash pour votre projet. Avez-vous absolument besoin de pour utiliser un framework JS? Si oui, avez-vous envisagé d'utiliser autre chose que React, comme Preact ou HyperHTML qui sont moins de 1/20 de la taille de React? Avez-vous besoin de TweenMax pour cette animation scroll-to-top? La commodité du npm et des composants isolés dans les frameworks a un inconvénient: la première réponse des développeurs à un problème est devenue d'y jeter plus de JS. Quand tout ce que vous avez est un marteau, tout ressemble à un clou. Lorsque vous avez fini d'élaguer les mauvaises herbes et d'expédier moins de JS, essayez de l'expédier plus malin . Expédiez ce dont vous avez besoin, quand vous en avez besoin. Webpack 3 possède des caractéristiques étonnantes appelées découpage de code et importations dynamiques . Au lieu de regrouper tous vos modules JS dans un ensemble monolithique app.js il peut automatiquement fractionner le code en utilisant la syntaxe import () et le charger de manière asynchrone. Il faut utiliser des frameworks, des composants et le routage côté client pour en tirer parti. Disons que vous avez un morceau de code complexe qui alimente votre .mega-widget qui peut être sur un nombre illimité de pages. Vous pouvez simplement écrire ce qui suit dans votre fichier JS principal:

 if (document.querySelector ('. Mega-widget')) {
    import ('./ méga-widget');
}

Si votre application trouve le widget sur la page, elle chargera dynamiquement le code de support requis. Sinon, tout va bien. Aussi, Webpack a besoin de son propre runtime pour fonctionner, et il l'injecte dans tous les fichiers .js qu'il génère. Si vous utilisez le plugin commonChunks vous pouvez utiliser ce qui suit pour extraire le runtime dans son propre bloc :

 new webpack.optimize.CommonsChunkPlugin ({
  nom: 'runtime',
}),

Il supprimera l'exécution de tous vos autres morceaux dans son propre fichier, dans ce cas nommé runtime.js . Assurez-vous simplement de le charger avant votre bundle JS principal. Par exemple:

  

Puis il y a le sujet du code transpilé et des polyfills. Si vous écrivez du JavaScript moderne (ES6 +), vous utilisez probablement Babel pour le transférer dans du code compatible ES5. Transpiling non seulement augmente la taille du fichier en raison de toute la verbosité, mais aussi de la complexité, et il a souvent des régressions de performances comparées au code ES6 + natif. Avec cela, vous utilisez probablement le paquetage babel-polyfill et whatwg-fetch pour rafistoler des fonctionnalités manquantes dans les anciens navigateurs. Ensuite, si vous écrivez du code en utilisant async / await vous le transportez aussi en utilisant les générateurs nécessaires pour inclure le regenerator-runtime ... Le point est, vous ajoutez presque 100 kilo-octets à votre bundle JS, qui a non seulement une énorme taille de fichier, mais aussi un énorme coût d'analyse et d'exécution, afin de supporter les anciens navigateurs. Inutile de punir les utilisateurs de navigateurs modernes. Une approche que j'utilise, et que Philip Walton a couverte dans cet article est de créer deux paquets séparés et de les charger conditionnellement. Babel rend cette tâche facile avec babel-preset-env . Par exemple, vous avez un bundle pour supporter IE 11, et l'autre sans polyfills pour les dernières versions des navigateurs modernes. Un moyen sale mais efficace est de placer ce qui suit dans un script en ligne:

 (function () {
  essayez {
    new Fonction ('async () => {}') ();
  } catch (erreur) {
    // crée une balise de script pointant vers legacy-bundle.js;
    revenir;
  }
  // crée une balise de script pointant vers modern-bundle.js ;;
}) ();

Si le navigateur n'est pas capable d'évaluer une fonction asynchrone nous supposons qu'il s'agit d'un ancien navigateur et que nous expédions simplement le paquet polyfilled. Sinon, l'utilisateur obtient la variante soignée et moderne

Conclusion

Ce que nous aimerions que vous tiriez de cet article, c'est que JS est cher et devrait être utilisé avec parcimonie. Assurez-vous de tester les performances de votre site appareils bas de gamme, dans des conditions de réseau réelles. Votre site devrait se charger rapidement et être interactif dès que possible. Cela signifie expédier moins de JS, et expédier plus rapidement par tous les moyens nécessaires. Votre code doit toujours être minifié, divisé en paquets plus petits et gérables et chargé de manière asynchrone chaque fois que possible. Du côté serveur, assurez-vous que HTTP / 2 est activé pour des transferts parallèles plus rapides et que la compression gzip / Brotli réduit considérablement les tailles de transfert de votre JS. Et avec cela dit, je voudrais terminer par le suivant tweet:






Source link