Accélération instagram.com. partie 1

Ces dernières années, beaucoup de nouvelles choses sont apparues sur instagram.com . Beaucoup. Par exemple - outils de narration, filtres, outils créatifs, notifications, messages directs. Cependant, à mesure que le projet grandissait, tout cela a donné un triste effet secondaire, à savoir que les performances d'instagram.com ont commencé à décliner. Au cours de la dernière année, l'équipe de développement d'Instagram a fait des efforts continus pour résoudre ce problème. Cela a conduit au fait que le temps de chargement total du flux Instagram (page de flux) a diminué de près de 50%.



Aujourd'hui, nous publions une traduction du premier matériel d'une série d'articles consacrés à l'histoire de la façon dont instagram.com a été accéléré.

À propos de l'optimisation des performances des projets Web



Amélioration des performances au cours de la dernière année (flux Instagram, métrique Display Done, ms.)

L'une des approches les plus importantes pour améliorer les performances des applications Web consiste à hiérarchiser correctement les ressources de chargement et de traitement et à réduire les temps d'arrêt du navigateur pendant le chargement des pages. Dans notre cas, bon nombre de ces optimisations se sont révélées plus efficaces que la réduction de la taille du code. En règle générale, nous n'avons eu aucune plainte concernant la taille du code. C'était assez compact. Ses dimensions ont commencé à nous déranger seulement après que de nombreuses petites améliorations ont été apportées au projet (nous prévoyons également de parler de l'optimisation de la taille du code). Ces améliorations ont en outre eu moins d'impact sur le processus de développement du projet. Ils ont nécessité moins de changements de code et moins de refactoring. En conséquence, nous avons initialement concentré nos efforts précisément sur ce domaine, en commençant par les ressources de préchargement.

Une histoire sur le préchargement d'images, le code JavaScript et les matériaux nécessaires pour terminer les requêtes, ainsi que les précautions à prendre


Le principe général de nos optimisations était d'informer le navigateur le plus rapidement possible des ressources nécessaires pour charger la page. En tant que développeurs de projets, dans de nombreux cas, nous savions à l'avance ce qui serait exactement nécessaire pour cela. Mais le navigateur ne pouvait en avoir aucune idée jusqu'à ce qu'une certaine partie du matériel de la page soit chargée et traitée. Les ressources en question, pour la plupart, comprenaient celles qui sont chargées dynamiquement à l'aide de JavaScript (par exemple, d'autres scripts, images, matériaux nécessaires pour exécuter les requêtes XHR). Le fait est que le navigateur ne peut pas détecter ces ressources dépendantes tant qu'il n'a pas analysé et exécuté du code JavaScript.

Au lieu d'attendre que le navigateur lui-même trouve ces ressources, nous pourrions lui donner un indice, après quoi il pourrait immédiatement commencer à les télécharger. Nous l'avons fait en utilisant les attributs HTML de preload . Cela ressemble à ceci:

 <link rel="preload" href="my-js-file.js" as="script" type="text/javascript" /> 

Nous utilisons des conseils similaires pour deux types de ressources sur les chemins de chargement des pages critiques. Il s'agit de code JavaScript chargé dynamiquement et de matériaux chargés dynamiquement de demandes de données GraphQL XHR. Les scripts chargés dynamiquement sont des scripts qui sont chargés à l'aide de constructions de l' import('...') formulaire import('...') pour des itinéraires client spécifiques. Nous maintenons une liste de correspondance des points d'entrée du serveur et des scripts de routage client. Par conséquent, lorsque nous recevons, sur le serveur, une demande de chargement de la page, nous connaissons les scripts pour lesquels les itinéraires client que vous devez télécharger. En conséquence, nous pouvons, lors de la génération du code HTML de la page, lui ajouter des conseils appropriés.

Par exemple, lorsque vous travaillez avec le point d'entrée FeedPage nous savons que le routeur client finira par terminer une demande de téléchargement de FeedPageContainer.js . Par conséquent, nous pouvons ajouter la construction suivante au code de la page:

 <link rel="preload" href="/static/FeedPageContainer.js" as="script" type="text/javascript" /> 

De même, si nous savons qu'une requête GraphQL est prévue pour être exécutée pour le point d'entrée d'une page particulière, cela signifie que nous devons précharger des matériaux pour accélérer l'exécution de cette requête. Ceci est particulièrement important en raison du fait que l'exécution de telles requêtes GraphQL prend parfois beaucoup de temps et que la page ne peut pas être rendue tant que les résultats de la requête ne sont pas retournés. Pour cette raison, nous devons faire en sorte que le serveur s'engage le plus tôt possible dans la formation des réponses à de telles demandes.

 <link rel="preload" href="/graphql/query?id=12345" as="fetch" type="application/json" /> 

Les changements dans les fonctionnalités de chargement des pages sont particulièrement visibles sur les connexions lentes. En simulant une connexion 3G rapide (le premier graphique en cascade ci-dessous, qui illustre la situation lorsque le préchargement des ressources n'est pas utilisé), nous pouvons voir que le chargement de FeedPageContainer.js et l'exécution de la requête GraphQL qui lui est associée ne commencent qu'après le chargement de Consumer.js . Toutefois, dans le cas où le préchargement est utilisé, le chargement du script FeedPageContainer.js et l'exécution de la requête GraphQL peuvent commencer immédiatement après que la page HTML est disponible. De plus, cela réduit le temps requis pour télécharger les scripts mineurs qui utilisent des mécanismes de chargement paresseux. Ici, FeedSidebarContainer.js et ActivityFeedBox.js (qui dépendent de FeedPageContainer.js ) commencent à se charger presque immédiatement après le traitement de Consumer.js .


Précharge non utilisée


Précharge utilisée

Avantages de prioriser la précharge


En plus d'utiliser l'attribut de preload pour démarrer plus rapidement le chargement des ressources, l'utilisation de ce mécanisme présente un autre avantage. Elle consiste à augmenter la priorité réseau de chargement de script asynchrone. Cela devient important lors de l'utilisation de scripts chargés de manière asynchrone dans des chemins critiques pour charger des pages, car par défaut, ils se chargent avec une faible priorité. Par conséquent, la priorité des demandes XHR et des images liées à la zone de page visible par les utilisateurs sera plus élevée que celle des documents en dehors de la zone de visualisation. Mais cela peut conduire à des situations où les scripts critiques nécessaires au rendu de la page sont bloqués ou forcés de partager la bande passante avec d'autres ressources. Si vous êtes intéressé, voici un compte rendu détaillé des priorités des ressources de Chrome. Une utilisation réfléchie du mécanisme de préchargement (nous en parlerons plus loin ci-dessous) donne au développeur un certain niveau de contrôle sur la façon dont le navigateur priorise le processus de chargement initial de la page. Cela est particulièrement vrai dans les cas où le développeur sait quelles ressources sont importantes pour l'affichage correct de la page.

Problèmes de priorisation de préchargement


Le problème du préchargement des ressources réside précisément dans le fait qu'il offre au développeur un levier supplémentaire pour influencer la priorité de chargement des ressources. Cela signifie que le développeur a plus de responsabilité pour la bonne priorisation. Par exemple, lors du test d'un site dans des régions où la vitesse des réseaux mobiles et WiFi est très faible, et où un pourcentage important de perte de paquets est observé, nous avons remarqué que la requête exécutée lors du traitement de <link rel="preload" as="script"> obtient une priorité plus élevée que la demande qui est exécutée lors du traitement de la <script /> ensembles JavaScript utilisés dans les chemins de rendu de page critiques. Cela entraîne une augmentation du temps de chargement global des pages.

La source de ce problème était la façon dont nous avons placé les balises de préchargement sur nos pages. À savoir, nous avons ajouté des astuces de préchargement uniquement pour les bundles, qui font partie de la page actuelle, que nous allions charger de manière asynchrone avec le routeur client.

 <!--   ,    --> <link rel="preload" href="SomeConsumerRoute.js" as="script" /> <link rel="preload" href="..." as="script" /> ... <!-- ,      --> <script src="Common.js" type="text/javascript"></script> <script src="Consumer.js" type="text/javascript"></script> 

Par exemple, sur la page de déconnexion, nous chargeons SomeConsumerRoute.js dans Common.js et Consumer.js , et puisque les ressources de préchargement sont chargées avec une priorité plus élevée, mais elles ne sont pas analysées, cela bloque Common.js et Consumer.js parsing Consumer.js . L'équipe de développement de Chrome Data Saver a détecté un problème de préchargement similaire et a décrit sa solution à ce problème. Dans leur cas, il a été décidé de toujours placer des constructions pour le préchargement des ressources asynchrones après les balises <script /> de ces ressources qui utilisent ces ressources asynchrones. Nous avons décidé de précharger tous les scripts et de placer les constructions correspondantes dans le code dans l'ordre dans lequel elles seraient nécessaires. Cela nous donne la possibilité de commencer à précharger toutes les ressources de script de la page le plus rapidement possible. Cela inclut des balises pour le chargement synchrone de scripts qui ne peuvent pas être ajoutés au HTML jusqu'à ce que des données de serveur spécifiques soient placées sur la page. Cela nous permet de contrôler l'ordre de chargement des scripts.

Voici le balisage qui précharge tous les bundles JavaScript.

 <!--      --> <link rel="preload" href="Common.js" as="script" /> <link rel="preload" href="Consumer.js" as="script" /> <!--   ,    --> <link rel="preload" href="SomeConsumerRoute.js" as="script" /> ... <!-- ,      --> <script src="Common.js" type="text/javascript"></script> <script src="Consumer.js" type="text/javascript"></script> <script src="SomeConsumerRoute.js" type="text/javascript" async></script> 

Précharge d'image


L'un des principaux domaines de travail d'instagram.com est Feed. Il s'agit d'une page d'image et de vidéo qui prend en charge le défilement sans fin. Nous remplissons cette page comme ceci. Tout d'abord, téléchargez l'ensemble initial de publications, puis, lorsque l'utilisateur fait défiler la page, chargez des ensembles supplémentaires de documents. Cependant, nous ne voudrions pas que l'utilisateur attende que de nouveaux matériaux soient chargés chaque fois qu'il atteint le bas de la bande. Par conséquent, afin de faciliter le travail avec cette page, nous téléchargeons de nouveaux ensembles de documents avant que l'utilisateur n'atteigne la fin de la bande.

En pratique, cela, pour plusieurs raisons, n'est pas une tâche facile:

  • Nous devons télécharger des documents qui ne sont pas visibles par l'utilisateur, afin qu'ils ne prennent pas les ressources réseau et processeur des documents qu'il consulte.
  • Nous ne voudrions pas transmettre des données inutiles sur le réseau, en essayant trop fort de précharger des publications que l'utilisateur pourrait même ne pas voir. Mais, d'autre part, si nous ne préchargeons pas une quantité suffisante de matériel, cela signifiera souvent le risque que l'utilisateur «se heurte» à la fin de la bande.
  • Le projet instagram.com est conçu pour fonctionner sur différents appareils et sur des écrans de différentes tailles. Par conséquent, nous srcset les images sur la bande à l'aide de l'attribut srcset de la <img> . Cet attribut permet au navigateur, compte tenu de la taille de l'écran, de décider quelle résolution d'image utiliser. Cela signifie qu'il n'est pas si facile pour nous de déterminer à l'avance la résolution des images à télécharger. De plus, il existe un risque de préchargement d'images que le navigateur n'utilisera pas.

L'approche que nous avons utilisée pour résoudre ce problème était de créer une abstraction de la tâche de priorisation, qui est responsable de la mise en file d'attente des tâches asynchrones (dans ce cas, il s'agit de tâches de préchargement du prochain ensemble de publications pour la sortie dans la bande). Une tâche similaire est initialement mise en file d'attente avec priorité idle (ici, requestIdleCallback utilisé). Cela signifie que l'exécution d'une telle tâche ne démarrera pas tant que le navigateur sera occupé par tout autre travail important. Cependant, si l'utilisateur fait défiler la page suffisamment près de l'endroit où se termine l'ensemble actuel des publications téléchargées, la priorité de cette tâche de préchargement des documents passe à high . Cela se fait en annulant le rappel de veille, après quoi le processus de préchargement démarre immédiatement.


Au début et au milieu de la bande, la tâche de préchargement des données a une priorité inactive et à la fin de la bande, la priorité est élevée

Une fois le téléchargement des données JSON terminé pour le prochain lot de publications, nous mettons en file d'attente une tâche d'arrière-plan répétitive pour le préchargement des images de ce lot. Le préchargement d'image est effectué dans l'ordre dans lequel les publications sont affichées dans le flux plutôt qu'en parallèle. Cela nous permet de hiérarchiser les tâches de chargement des données et d'afficher des images pour les publications les plus proches de l'endroit sur la page que l'utilisateur voit. Pour télécharger des images de la bonne taille, nous utilisons un composant média caché, dont les paramètres correspondent aux paramètres de la bande actuelle. À l'intérieur de ce composant, il y a un élément <img> qui utilise l'attribut srcset , le même qui est utilisé pour afficher les publications réelles dans le flux. Cela signifie que nous pouvons fournir au navigateur la possibilité de prendre des décisions sur les images à précharger. Par conséquent, le navigateur utilisera la même logique lors de l'affichage des images qu'il a utilisé lors de leur préchargement. Cela signifie également que nous, en utilisant un composant multimédia similaire, pouvons précharger des images pour d'autres zones du site. Telles que les pages de profil utilisateur.

L'effet global des améliorations ci-dessus a entraîné une réduction de 25% du temps requis pour télécharger des photos. Nous parlons de la durée entre le moment où le code de publication est ajouté au DOM et le moment où l'image de la publication est chargée et affichée. En outre, cela a entraîné une réduction de 56% du temps que les utilisateurs, ayant atteint la fin du flux, passaient à attendre pour télécharger de nouveaux documents.

Chers lecteurs! Utilisez-vous des mécanismes de préchargement des données pour optimiser vos projets Web?


Source: https://habr.com/ru/post/fr468227/


All Articles