Front-end sophistiqué. La bonne architecture pour les sites rapides

Bonjour, Habr!



Nous avons longtemps ignoré le sujet des navigateurs, du CSS et de l'accessibilité et avons décidé d'y revenir avec la traduction du matériel d'évaluation d'aujourd'hui (original - février 2020). Je suis particuliÚrement intéressé par votre opinion sur la technologie de rendu de serveur mentionnée ici, ainsi que sur l'urgence du besoin d'un livre à part entiÚre sur HTTP / 2 - cependant, parlons de tout dans l'ordre.



Cet article décrit quelques techniques pour accélérer le chargement des applications frontales et ainsi améliorer la convivialité.



Jetons un coup d'Ɠil Ă  l'architecture gĂ©nĂ©rale du frontend. Comment vous assurer que les ressources critiques se chargent en premier et maximiser la probabilitĂ© que ces ressources se retrouvent dĂ©jĂ  dans le cache?



Je ne m'attarderai pas sur la façon dont le backend doit fournir des ressources, si vous avez besoin de créer votre page sous la forme d'une application cliente, ou comment optimiser le temps de rendu de votre application.



Aperçu



Décomposons le processus de téléchargement de l'application en trois étapes distinctes:



  1. Rendu principal - combien de temps faudra-t-il avant que l'utilisateur ne voie quelque chose?
  2. Téléchargement de l'application - combien de temps faudra-t-il avant que l'utilisateur ne voie l'application?
  3. – ?




Jusqu'au stade du rendu principal (rendu), l'utilisateur ne peut tout simplement rien voir. Pour rendre une page, vous avez besoin d'au moins un document HTML, mais dans la plupart des cas, vous devez également charger des ressources supplémentaires, telles que des fichiers CSS et JavaScript. S'ils sont disponibles, le navigateur peut commencer le rendu à l'écran.



Dans cet article, j'utiliserai des diagrammes en cascade WebPageTest . La cascade de demandes pour votre site ressemblera Ă  ceci.







Un tas d'autres fichiers sont chargĂ©s avec le document HTML et la page s'affiche une fois qu'ils sont tous en RAM. Veuillez noter que les fichiers CSS sont chargĂ©s en parallĂšle, de sorte que chaque requĂȘte ultĂ©rieure n'augmente pas significativement le dĂ©lai.



RĂ©duire le nombre de requĂȘtes de blocage de rendu



Les feuilles de style et (par défaut) les éléments de script ne permettent pas d'afficher le contenu en dessous d'eux.



Il existe plusieurs façons de résoudre ce problÚme:



  • Nous mettons des balises de script tout en bas de la balise body
  • Charger des scripts de maniĂšre asynchrone Ă  l'aide de async
  • Écrivez de petits morceaux de JS ou CSS en ligne si vous souhaitez les charger de maniĂšre synchrone


Évitez de bloquer le rendu des chaĂźnes de requĂȘtes



Ce n'est pas seulement le nombre de demandes de blocage de rendu qui peuvent ralentir votre site. Ce qui compte, c'est la taille de chacune de ces ressources Ă  tĂ©lĂ©charger, ainsi que le moment exact oĂč le navigateur dĂ©tecte que la ressource doit ĂȘtre tĂ©lĂ©chargĂ©e.



Si le navigateur prend conscience de la nécessité de télécharger le fichier uniquement aprÚs qu'une autre demande est terminée, vous pouvez vous retrouver avec une chaßne de demandes synchrones. Cela peut se produire pour plusieurs raisons:



  • Avoir des rĂšgles @importen CSS
  • Utilisation de polices Web rĂ©fĂ©rencĂ©es dans un fichier CSS
  • Lien d'injection JavaScript ou balises de script


Considérez cet exemple: l'







un des fichiers CSS de ce site contient une rĂšgle @importde chargement d'une police Google. Ainsi, le navigateur doit exĂ©cuter les requĂȘtes suivantes une par une, dans cet ordre:



  1. HTML du document
  2. CSS d'application
  3. Google Fonts CSS
  4. Fichier Google Font Woff (non affiché en cascade)


Pour résoudre ce problÚme, nous déplaçons d'abord la demande de CSS Google Fonts @importvers la balise de lien dans le document HTML. Cela raccourcira la chaßne d'un maillon.



Pour encore plus d'accélération, intégrez le fichier CSS Google Fonts directement dans votre HTML ou votre fichier CSS.



(N'oubliez pas que la rĂ©ponse CSS de Google Fonts dĂ©pend de l'agent utilisateur. Si vous faites une demande Ă  l'aide d'IE8, le CSS rĂ©fĂ©rencera un fichier EOT (intĂ©grĂ© par OpenType), IE11 recevra un fichier woff et les navigateurs modernes recevront un woff2. Mais si vous en ĂȘtes satisfait fonctionnent comme avec des navigateurs relativement anciens utilisant des polices systĂšme, vous pouvez simplement copier et coller le contenu du fichier CSS.)



MĂȘme aprĂšs le dĂ©but du rendu de la page, l'utilisateur peut ne pas pouvoir faire quoi que ce soit, car aucun texte n'est affichĂ© tant que la police n'est pas complĂštement chargĂ©e. Cela peut ĂȘtre Ă©vitĂ© en utilisant la propriĂ©tĂ© de permutation d'affichage des polices , qui est dĂ©sormais la valeur par dĂ©faut dans Google Fonts.



Parfois, il n'est pas possible de se dĂ©barrasser complĂštement de la chaĂźne de demandes. Dans ce cas, essayez d'utiliser la balise preloadou preconnect. Par exemple, le site illustrĂ© ci-dessus peut se connecter fonts.googleapis.comavant que la requĂȘte CSS rĂ©elle ne soit effectuĂ©e.



RĂ©utilisation des connexions serveur pour accĂ©lĂ©rer les requĂȘtes



En rÚgle générale, l'établissement d'une nouvelle connexion au serveur nécessite 3 passes aller-retour entre le navigateur et le serveur:



  1. Recherche DNS
  2. Établissement d'une connexion TCP
  3. Établir une connexion SSL


Une fois la connexion Ă©tablie, il faut au moins 1 aller-retour supplĂ©mentaire: envoyer une requĂȘte et tĂ©lĂ©charger une rĂ©ponse.



Comme indiqué dans la cascade ci-dessous, les connexions sont initiées vers quatre serveurs différents: hostgator.com, optimizely.com, googletagmanager.com et googelapis.com.



Cependant , les demandes ultérieures adressées au serveur concerné peuvent réutiliser la connexion existante. Par conséquent, base.cssou index1.csssont chargés rapidement, car ils se trouvent également sur hostgator.com.







Réduction de la taille des fichiers et utilisation des réseaux de diffusion de contenu (CDN)



La longueur de la demande, ainsi que la taille du fichier, est influencée par deux autres facteurs que vous contrÎlez: la taille de la ressource et l'emplacement de vos serveurs.



Envoyez à l'utilisateur la quantité minimale de données requise, en plus, prenez soin de leur compression (par exemple, en utilisant brotli ou gzip).



Les réseaux de diffusion de contenu (CDN) fournissent des serveurs dans une grande variété d'emplacements, il y a donc de fortes chances que l'un d'eux soit situé à proximité de vos utilisateurs. Vous pouvez les connecter non pas à votre serveur d'applications central, mais au serveur le plus proche sur le CDN. Ainsi, le chemin des données vers le serveur et retour sera considérablement réduit. Ceci est particuliÚrement utile lorsque vous travaillez avec des ressources statiques telles que CSS, JavaScript et images, car elles sont faciles à distribuer.



Contournement du réseau avec les techniciens de service



Les techniciens de service vous permettent d'intercepter les demandes avant qu'elles n'entrent sur le réseau. Ainsi, le premier rendu peut se produire presque instantanément !







Bien sĂ»r, cela ne fonctionne que si vous souhaitez que le rĂ©seau envoie simplement une rĂ©ponse. Cette rĂ©ponse doit dĂ©jĂ  ĂȘtre mise en cache, facilitant ainsi la vie de vos utilisateurs lorsqu'ils tĂ©lĂ©chargent Ă  nouveau votre application.



Le technicien de service illustré ci-dessous met en cache le HTML et le CSS requis pour afficher la page. Lors du rechargement, l'application essaie d'émettre des ressources mises en cache, et si elles ne sont pas disponibles, elle se tourne vers le réseau comme solution de secours.



self.addEventListener("install", async e => {
 caches.open("v1").then(function (cache) {
   return cache.addAll(["/app", "/app.css"]);
 });
});

self.addEventListener("fetch", event => {
 event.respondWith(
   caches.match(event.request).then(cachedResponse => {
     return cachedResponse || fetch(event.request);
   })
 );
});


Pour plus d'informations sur le préchargement et la mise en cache des ressources à l'aide des services workers, consultez ce didacticiel .



Téléchargement de l'application



D'accord, notre utilisateur a déjà vu quelque chose. De quoi d'autre a-t-il besoin pour pouvoir utiliser notre application?



  1. Chargement de l'application (JS et CSS)
  2. Chargement des données les plus importantes pour une page
  3. Téléchargez des données et des images supplémentaires






Veuillez noter que non seulement le chargement de données sur le réseau peut ralentir le rendu. Lorsque le code est chargé, le navigateur devra l'analyser, le compiler et l'exécuter.



Fractionnement du bundle: chargez uniquement le code nécessaire et maximisez les accÚs au cache.



En divisant le bundle, vous pouvez tĂ©lĂ©charger uniquement le code dont vous avez besoin uniquement pour cette page, et non pas tĂ©lĂ©charger l'application entiĂšre. Lors du fractionnement d'un bundle, il peut ĂȘtre mis en cache en plusieurs parties, mĂȘme si d'autres parties du code ont changĂ© et doivent ĂȘtre rechargĂ©es.



En rÚgle générale, le code se compose de trois types de fichiers différents:



  • Code spĂ©cifique Ă  cette page
  • Code d'application partagĂ©
  • Modules tiers qui changent rarement (idĂ©al pour la mise en cache!)


Webpack peut automatiquement diviser le code fractionnĂ© pour rĂ©duire le poids total des tĂ©lĂ©chargements, cela se fait Ă  l'aide de Optimization.splitChunks . Assurez-vous d'activer le bloc d'exĂ©cution afin que les hachages des blocs restent stables et que la mise en cache Ă  long terme puisse ĂȘtre utilisĂ©e de maniĂšre utile. Ivan Akulov a Ă©crit un guide complet sur le partage et la mise en cache du code Webpack.



Le fractionnement du code spĂ©cifique Ă  la page ne peut pas ĂȘtre effectuĂ© automatiquement, vous devez donc identifier les extraits qui peuvent ĂȘtre chargĂ©s sĂ©parĂ©ment. Il s'agit souvent d'un itinĂ©raire ou d'un ensemble de pages spĂ©cifiques. Utilisez les importations dynamiques pour charger paresseusement ce code.



Le fractionnement du bundle entraĂźne davantage de demandes pour charger complĂštement votre application. Mais, si les requĂȘtes sont parallĂ©lisĂ©es, ce problĂšme n'est pas gros, en particulier sur les sites utilisant HTTP / 2. Notez les trois premiĂšres requĂȘtes de cette cascade:







Cependant, cette cascade montre Ă©galement 2 requĂȘtes exĂ©cutĂ©es en sĂ©quence. Ces fragments ne sont nĂ©cessaires que pour cette page et sont chargĂ©s dynamiquement Ă  l'aide d'un appel import().



Vous pouvez résoudre ce problÚme en insérant une balise preload linksi vous savez que vous aurez certainement besoin de ces fragments.







Cependant, comme vous pouvez le voir, le gain de vitesse dans ce cas peut ĂȘtre faible par rapport au temps de chargement total de la page.



De plus, l'utilisation du prĂ©chargement est parfois contre-productive et peut entraĂźner des retards lorsque d'autres fichiers plus importants sont chargĂ©s. Consultez l'article d' Andy Davis sur le prĂ©chargement des polices et comment bloquer le rendu principal en chargeant d'abord les polices, puis le CSS qui empĂȘche le rendu.



Chargement des données de la page



Probablement, votre application est conçue pour afficher un certain type de données. Voici quelques conseils sur la façon de charger les données à l'avance et d'éviter les retards de rendu.



N'attendez pas les bundles, commencez tout de suite à charger les données.



Il peut y avoir un cas particulier de chaĂźnage de requĂȘtes sĂ©quentielles: vous chargez un bundle d'application, et ce code demande dĂ©jĂ  des donnĂ©es de page.



Il existe deux façons d'éviter cela:



  1. Incorporer les données de la page dans un document HTML
  2. Commencez à demander des données via un script en ligne à l'intérieur du document


L'intégration des données dans HTML garantit que votre application n'a pas à attendre son chargement. Cela réduit également la complexité globale de l'application en n'ayant pas à gérer l'état de chargement.



Cependant, cette idée n'est pas si bonne si la récupération des données entraßne un retard important dans la réponse de votre document, car elle ralentira également le rendu initial.



Dans ce cas, ou lors de la diffusion d'un document HTML mis en cache Ă  l'aide d'un service worker, vous pouvez incorporer un script en ligne dans le HTML qui chargera ces donnĂ©es. Il peut ĂȘtre fourni sous forme de promesse globale, comme ceci:



window.userDataPromise = fetch("/me")


Ensuite, si les donnĂ©es sont dĂ©jĂ  prĂȘtes, votre application peut immĂ©diatement dĂ©marrer le rendu ou attendre qu'elle soit prĂȘte.



Lorsque vous utilisez ces deux mĂ©thodes, vous devez savoir exactement quelles donnĂ©es doivent ĂȘtre affichĂ©es sur la page, et mĂȘme avant que l'application ne commence le rendu. Cela est gĂ©nĂ©ralement facile Ă  fournir pour les donnĂ©es spĂ©cifiques Ă  l'utilisateur (nom, notifications ...) mais pas facile lorsqu'il s'agit de contenu spĂ©cifique Ă  une page. Essayez de mettre vous-mĂȘme en Ă©vidence les pages les plus importantes et Ă©crivez votre propre logique pour chacune d'elles.



Ne bloquez pas le rendu en attendant des données non pertinentes



Parfois, la génération de données paginées nécessite une logique lente et complexe implémentée dans le backend. Dans de tels cas, la possibilité de charger une version simplifiée des données est d'abord utile, si cela suffit pour rendre votre application fonctionnelle et interactive.



Par exemple, un outil analytique peut charger tous les graphiques d'abord, puis les accompagner de donnĂ©es. Ainsi, l'utilisateur peut immĂ©diatement consulter le schĂ©ma qui l'intĂ©resse, et vous aurez le temps de rĂ©partir les requĂȘtes backend sur diffĂ©rents serveurs.







Évitez les chaĂźnes de requĂȘtes de donnĂ©es sĂ©quentielles



Ce conseil peut sembler contredire mon point prĂ©cĂ©dent oĂč j'ai parlĂ© de reporter le chargement de donnĂ©es non pertinentes Ă  une deuxiĂšme demande. Cependant, Ă©vitez de chaĂźner des requĂȘtes consĂ©cutives si une requĂȘte ultĂ©rieure dans la chaĂźne ne fournit pas de nouvelles informations Ă  l'utilisateur.



Au lieu de demander d'abord à quel utilisateur est connecté, puis de demander une liste de groupes auxquels appartient l'utilisateur, renvoyez une liste de groupes avec des informations sur l'utilisateur. Vous pouvez utiliser GraphQL pour cela , mais un point de terminaison personnalisé convient user?includeTeams=trueégalement.



Rendu cÎté serveur



Dans ce cas, nous entendons le rendu avancĂ© de l'application sur le serveur, de sorte qu'une page HTML Ă  part entiĂšre soit servie en rĂ©ponse Ă  une requĂȘte d'un document. De cette façon, le client peut voir la page entiĂšre sans avoir Ă  attendre le chargement de code ou de donnĂ©es supplĂ©mentaires!



Étant donnĂ© que le serveur n'envoie que du HTML statique au client, votre application est toujours dĂ©pourvue d'interactivitĂ© Ă  ce stade. L'application doit ĂȘtre chargĂ©e, elle doit rĂ©exĂ©cuter la logique de rendu, puis attacher les Ă©couteurs d'Ă©vĂ©nements requis au DOM.



Utilisez le rendu cÎté serveur lorsque vous trouvez que le contenu non interactif est précieux en soi. En outre, cette approche permet de mettre en cache le code HTML qui y était affiché sur le serveur, puis de le transférer à tous les utilisateurs sans délai lors de la premiÚre demande du document. Par exemple, le rendu cÎté serveur est idéal si vous affichez un blog à l'aide de React.



Lisez cet article de Michal Janaszek; il décrit bien comment combiner les techniciens de service avec le rendu cÎté serveur.



Page suivante



À un moment donnĂ©, l'utilisateur travaillant avec votre application devra passer Ă  la page suivante. Lorsque la premiĂšre page est ouverte, vous contrĂŽlez tout ce qui se passe dans le navigateur, vous pouvez donc vous prĂ©parer Ă  la prochaine interaction.



Prérécupération des ressources La



prélecture du code requis pour afficher la page suivante peut aider à éviter les retards dans la navigation personnalisée. Utilisez des balises prefetch linkou webpackPrefetchpour des importations dynamiques:



import(
    /* webpackPrefetch: true, webpackChunkName: "todo-list" */ "./TodoList"
)


Tenez compte de la quantitĂ© de donnĂ©es utilisateur que vous utilisez et de la bande passante, en particulier en ce qui concerne la connexion mobile. C'est dans la version mobile du site que vous ne pouvez pas ĂȘtre zĂ©lĂ© avec le prĂ©chargement, et aussi si le mode d'Ă©conomie de donnĂ©es est activĂ©.



Sélectionnez stratégiquement les données dont vos utilisateurs ont le plus besoin.



Réutilisez les données déjà chargées



Mettez en cache localement les donnĂ©es Ajax dans votre application pour Ă©viter les demandes inutiles ultĂ©rieurement. Si l'utilisateur accĂšde Ă  la liste des groupes sur la page Modifier le groupe, la transition peut ĂȘtre effectuĂ©e instantanĂ©ment en rĂ©utilisant les donnĂ©es dĂ©jĂ  sĂ©lectionnĂ©es prĂ©cĂ©demment.



Veuillez noter que cela ne fonctionnera pas si votre objet est fréquemment modifié par d'autres utilisateurs et que les données que vous avez téléchargées peuvent rapidement devenir obsolÚtes. Dans de tels cas, essayez d'abord d'afficher les données existantes en mode lecture seule et en attendant de sélectionner les données mises à jour.



Conclusion



Dans cet article, nous avons examiné un certain nombre de facteurs qui peuvent ralentir une page à différents moments du processus de chargement. Utilisez des outils tels que Chrome DevTools , WebPageTest et Lighthouse pour déterminer les conseils pertinents pour votre application.



Dans la pratique, il est rarement possible de réaliser une optimisation complÚte. Déterminez ce qui est le plus important pour vos utilisateurs et concentrez-vous sur cela.



En travaillant sur cet article, j'ai rĂ©alisĂ© que je partageais une conviction profondĂ©ment ancrĂ©e selon laquelle plusieurs requĂȘtes sont de mauvais problĂšmes de performances. Cela Ă©tait vrai dans le passĂ©, lorsque chaque demande nĂ©cessitait une connexion distincte et que les navigateurs n'autorisaient que quelques connexions par domaine. Mais ce problĂšme a disparu avec l'avĂšnement de HTTP / 2 et des navigateurs modernes.



Il existe de solides arguments en faveur du fractionnement des requĂȘtes. En faisant cela, vous pouvez charger les ressources strictement nĂ©cessaires et mieux utiliser le contenu mis en cache, car il vous suffit de recharger les fichiers qui ont changĂ©.



All Articles