Fermer

janvier 21, 2021

Comment nous avons amélioré les performances de Mag


À propos de l'auteur

Vitaly Friedman aime les beaux contenus et n'aime pas céder facilement. Lorsqu'il n'écrit pas ou ne prend pas la parole lors d'une conférence, il est probablement en train de courir…
En savoir plus sur
Vitaly

Dans cet article, nous allons examiner de près certaines des modifications que nous avons apportées sur ce site même – fonctionnant sur JAMStack avec React – pour optimiser les performances Web et améliorer les métriques Core Web Vitals. Avec certaines des erreurs que nous avons commises et certains des changements inattendus qui ont contribué à améliorer toutes les mesures à tous les niveaux.

Chaque histoire de performances Web est similaire, n'est-ce pas? Cela commence toujours par la refonte du site Web tant attendue. Un jour où un projet, entièrement peaufiné et soigneusement optimisé, est lancé, se classant bien et dépassant les scores de performance dans Lighthouse et WebPageTest. Il y a une célébration et un sentiment sincère d'accomplissement qui règne dans l'air – magnifiquement reflété dans les retweets, les commentaires, les newsletters et les fils de discussion Slack.

Pourtant, avec le temps, l'excitation s'estompe lentement, et des ajustements urgents, des fonctionnalités indispensables , et de nouvelles exigences métier s'infiltrent. Et soudainement, avant que vous ne le sachiez, la base de code devient un peu surpoids et fragmentée les scripts tiers doivent se charger juste un peu plus tôt et une nouvelle dynamique brillante le contenu trouve son chemin dans le DOM à travers les portes dérobées des scripts de quatrième partie et de leurs invités non invités.

Nous avons été là aussi à Smashing. Peu de gens le savent, mais nous sommes une très petite équipe d'environ 12 personnes, dont beaucoup travaillent à temps partiel et dont la plupart portent généralement de nombreux chapeaux différents un jour donné. Alors que la performance est notre objectif depuis près d'une décennie maintenant nous n'avons jamais vraiment eu d'équipe de performance dédiée.

Après la dernière refonte fin 2017, c'était Ilya Pukhalski sur le Du côté JavaScript des choses (à temps partiel), Michael Riethmueller du côté CSS des choses (quelques heures par semaine), et le vôtre vraiment, jouer à des jeux d’esprit avec du CSS critique et essayer de jongler avec un peu trop

 Capture d'écran des sources de performances montrant les scores de Lighthouse entre 40 et 60
C'est ici que nous avons commencé. Les scores de Lighthouse se situant entre 40 et 60, nous avons décidé de s'attaquer (encore une fois) à la performance. (Source de l'image: Lighthouse Metrics ) ( Grand aperçu )

En l'occurrence, nous avons perdu la trace de la performance dans l'agitation de la routine quotidienne. Nous étions en train de concevoir et de construire des choses, de mettre en place de nouveaux produits, de refactoriser les composants et de publier des articles. Ainsi, à la fin de 2020, les choses sont devenues un peu incontrôlables, avec les scores de Lighthouse rouge jaunâtre apparaissant lentement à tous les niveaux. Nous avons dû résoudre ce problème.

C'est là que nous étions

Certains d'entre vous savent peut-être que nous fonctionnons sur JAMStack avec tous les articles et pages stockés sous forme de fichiers Markdown, fichiers Sass compilés en CSS, JavaScript divisé en morceaux avec Webpack et Hugo créant des pages statiques que nous servons ensuite directement à partir d'un CDN Edge. En 2017, nous avons construit l'ensemble du site avec Preact, mais nous sommes ensuite passés à React en 2019 – et l'avons utilisé avec quelques API pour la recherche, les commentaires, l'authentification et le paiement.

L'ensemble du site est construit avec progressif amélioration à l'esprit, ce qui signifie que vous, cher lecteur, pouvez lire chaque article de Smashing dans son intégralité sans avoir à démarrer l'application du tout. Ce n'est pas très surprenant non plus – en fin de compte, un article publié ne change pas beaucoup au fil des ans, tandis que les éléments dynamiques tels que l'authentification des membres et le paiement nécessitent que l'application s'exécute.

L'ensemble de la compilation pour déployer environ 2500 articles en direct prend vers 6 minutes pour le moment. Le processus de construction en lui-même est également devenu une bête avec le temps, avec des injections CSS critiques, la division du code de Webpack, des insertions dynamiques de publicités et de panneaux de fonctionnalités, la (re) génération de RSS et les tests A / B éventuels à la périphérie. [19659006] Début 2020, nous avons commencé le grand refactoring des composants de mise en page CSS. Nous n'avons jamais utilisé CSS-in-JS ou styled-components, mais plutôt un bon vieux système de modules Sass basé sur des composants qui serait compilé en CSS. En 2017, la mise en page entière a été construite avec Flexbox et reconstruite avec CSS Grid et CSS Custom Properties à la mi-2019. Cependant, certaines pages nécessitaient un traitement spécial en raison de nouveaux spots publicitaires et de nouveaux panneaux de produits. Ainsi, pendant que la mise en page fonctionnait, elle ne fonctionnait pas très bien et était assez difficile à maintenir.

De plus, l'en-tête avec la navigation principale a dû changer pour accueillir plus d'éléments que nous voulions afficher dynamiquement. De plus, nous voulions refactoriser certains composants fréquemment utilisés sur le site, et le CSS utilisé là-bas nécessitait également une révision – la boîte de newsletter étant le coupable le plus notable. Nous avons commencé par refactoriser certains composants avec CSS avant tout, mais nous n'avons jamais été au point qu'il était utilisé de manière cohérente sur l'ensemble du site.

Le plus gros problème était le grand ensemble JavaScript qui – pas très étonnamment – bloquait le thread principal pendant des centaines de millisecondes. Un gros bundle JavaScript peut sembler déplacé sur un magazine qui ne fait que publier des articles, mais en fait, il y a beaucoup de scripts en coulisses.

Nous avons différents états de composants pour les clients authentifiés et non authentifiés. Une fois que vous êtes connecté, nous voulons afficher tous les produits dans le prix final, et lorsque vous ajoutez un livre au panier, nous voulons garder un panier accessible en appuyant sur un bouton, quelle que soit la page sur laquelle vous vous trouvez. La publicité doit arriver rapidement sans provoquer de changements de disposition perturbateurs et il en va de même pour les panneaux de produits natifs qui mettent en valeur nos produits. Plus un service worker qui met en cache tous les actifs statiques et les sert pour des vues répétées, ainsi que des versions en cache des articles qu'un lecteur a déjà visités.

Donc, tout ce script devait se produire à un certain point, et cela épuisait l'expérience de lecture même si le script arrivait assez tard. Franchement, nous travaillions minutieusement sur le site et les nouveaux composants sans surveiller de près les performances (et nous avions quelques autres choses à garder à l'esprit pour 2020). Le tournant est venu de façon inattendue. Harry Roberts a animé son (excellent) Web Performance Masterclass en tant qu'atelier en ligne avec nous, et tout au long de l'atelier, il a utilisé Smashing comme exemple en soulignant les problèmes que nous avions et en suggérant des solutions à ces problèmes en plus d'utiles

Tout au long de l'atelier, j'ai pris des notes avec diligence et revisité la base de code. Au moment de l'atelier, nos scores Lighthouse étaient de 60–68 sur la page d'accueil et autour de 40-60 sur les pages d'articles – et évidemment pires sur mobile. Une fois l'atelier terminé, nous nous sommes mis au travail.

Identifier les goulots d'étranglement

Nous avons souvent tendance à nous fier à des partitions particulières pour comprendre notre performance, mais trop souvent les partitions individuelles ne donnent pas une image complète . Comme David East l'a fait remarquer avec éloquence dans son article, les performances Web ne sont pas une valeur unique; c'est une distribution. Même si une expérience Web est fortement et complètement optimisée, elle ne peut pas être simplement rapide. Il peut être rapide pour certains visiteurs, mais en fin de compte, il sera aussi plus lent (ou lent) pour certains autres.

Les raisons en sont nombreuses, mais la plus importante est une énorme différence dans les conditions du réseau et le matériel des périphériques à travers le monde. Le plus souvent, nous ne pouvons pas vraiment influencer ces choses, nous devons donc nous assurer que notre expérience les accepte à la place.

Essentiellement, notre travail consiste alors à augmenter la proportion d'expériences accrocheuses et à réduire la proportion d'expériences lentes. Mais pour cela, nous devons avoir une image correcte de ce qu'est réellement la distribution. Désormais, les outils d'analyse et de surveillance des performances fourniront ces données en cas de besoin, mais nous nous sommes penchés spécifiquement sur CrUX Chrome User Experience Report. CrUX génère un aperçu des distributions de performances au fil du temps, avec le trafic collecté auprès des utilisateurs de Chrome. Une grande partie de ces données concernaient Core Web Vitals que Google a annoncé en 2020, et qui contribuent également et sont également exposées dans Lighthouse.

 Statistiques de Largest Contentful Paint (LCP) montrant une baisse massive des performances entre mai et septembre en 2020 [19659010] La distribution des performances pour la plus grande peinture de contenu en 2020. Entre mai et septembre, les performances ont chuté massivement. Données tirées de <a href= CrUX . ( Grand aperçu )

Nous avons remarqué que dans l'ensemble, notre performance régressait considérablement tout au long de l'année, avec des baisses particulières vers août et septembre. Une fois que nous avons vu ces graphiques, nous pourrions regarder en arrière dans certains des PR que nous avons poussés en direct à l'époque pour étudier ce qui s'est réellement passé.

Il n'a pas fallu un certain temps pour comprendre que juste à cette époque, nous avons lancé un nouvelle barre de navigation en direct. Cette barre de navigation – utilisée sur toutes les pages – reposait sur JavaScript pour afficher les éléments de navigation dans un menu au toucher ou au clic, mais la partie JavaScript de celle-ci était en fait intégrée dans le bundle app.js . Pour améliorer Time To Interactive, nous avons décidé d'extraire le script de navigation du bundle et de le diffuser en ligne.

À peu près au même moment, nous sommes passés d'un CSS critique (obsolète) créé manuellement ] vers un système automatisé qui générait du CSS critique pour chaque modèle – page d'accueil, article, page produit, événement, tableau des offres d'emploi, etc. – et CSS critique en ligne pendant la construction. Pourtant, nous n'avions pas vraiment réalisé à quel point le CSS critique généré automatiquement était plus lourd. Nous avons dû l'explorer plus en détail.

Et aussi à peu près au même moment, nous ajustions le chargement de la police Web essayant de pousser les polices Web plus agressivement avec des conseils de ressources tels que précharge. Cela semble cependant contrecarrer nos efforts de performance, car les polices Web retardaient le rendu du contenu, étant sur-priorisées à côté du fichier CSS complet.

Maintenant, l'une des raisons courantes de la régression est le coût élevé de JavaScript, donc nous avons également examiné Webpack Bundle Analyzer et la carte de requête de Simon Hearne pour obtenir une image visuelle de nos dépendances JavaScript. Cela semblait assez sain au début.

 Une carte mentale visuelle des dépendances JavaScript
Rien de vraiment révolutionnaire: la carte de requête ne semblait pas excessive au début. ( Grand aperçu )

Quelques demandes arrivaient sur le CDN, un service de consentement aux cookies Cookiebot, Google Analytics, ainsi que nos services internes de service de panneaux de produits et de publicité personnalisée. Il ne semblait pas y avoir beaucoup de goulots d'étranglement – jusqu'à ce que nous regardions d'un peu plus près.

Dans le travail de performance, il est courant de regarder les performances de certaines pages critiques – très probablement la page d'accueil et très probablement quelques articles / produits pages. Cependant, bien qu'il n'y ait qu'une seule page d'accueil, il peut y avoir beaucoup de pages de produits différentes, nous devons donc en choisir celles qui sont représentatives de notre public.

En fait, comme nous publions pas mal de code et de design -articles lourds sur SmashingMag, au fil des ans, nous avons accumulé littéralement des milliers d'articles contenant des GIF lourds, des extraits de code à syntaxe, des intégrations CodePen, des intégrations vidéo / audio et des fils imbriqués de commentaires sans fin.

ensemble, beaucoup d'entre eux ne causaient rien de moins qu'une explosion de la taille du DOM avec un travail excessif du fil principal – ralentissant l'expérience sur des milliers de pages. Sans oublier qu'avec la publicité en place, certains éléments DOM ont été injectés tard dans le cycle de vie de la page, provoquant une cascade de recalculs et de repeints de style – également des tâches coûteuses qui peuvent produire de longues tâches.

Tout cela n'apparaissait pas dans le map que nous avons générée pour une page d'article assez légère dans le graphique ci-dessus. Nous avons donc choisi les pages les plus lourdes que nous ayons – la toute-puissante page d'accueil la plus longue celle avec de nombreuses vidéos intégrées et celle avec de nombreux CodePen embarque – et a décidé de les optimiser autant que possible. Après tout, si elles sont rapides, alors les pages avec une seule intégration de CodePen devraient l'être aussi.

Avec ces pages à l'esprit, la carte avait un aspect un peu différent. Notez l'énorme ligne épaisse se dirigeant vers le lecteur Vimeo et le CDN Vimeo, avec 78 requêtes provenant d'un article Smashing.

 Une carte mentale visuelle montrant les problèmes de performances, en particulier dans les articles qui utilisaient de nombreuses vidéos et / ou vidéos intégrées
On certaines pages d'article, le graphique était différent. Surtout avec beaucoup d'intégrations de code ou de vidéo, les performances diminuaient considérablement. Malheureusement, beaucoup de nos articles en contiennent. ( Grand aperçu )

Pour étudier l'impact sur le thread principal, nous avons plongé dans le panneau Performances de DevTools. Plus précisément, nous recherchions des tâches qui durent plus de 50 secondes (surlignées d'un rectangle à droite dans le coin supérieur droit) et des tâches contenant des styles de recalcul (barre violette). Le premier indiquerait une exécution JavaScript coûteuse, tandis que le second exposerait des invalidations de style causées par des injections dynamiques de contenu dans le DOM et un CSS sous-optimal. Cela nous a donné des indications pratiques pour savoir par où commencer. Par exemple, nous avons rapidement découvert que le chargement de nos polices Web avait un coût de repeinture important, tandis que les blocs JavaScript étaient encore assez lourds pour bloquer le thread principal.

 Une capture d'écran du panneau de performances dans DevTools montrant des blocs JavaScript encore assez lourds pour bloquer le thread principal
Étude du panneau Performance dans DevTools. Il y avait quelques tâches longues, prenant plus de 50 ms et bloquant le thread principal. ( Grand aperçu )

Comme base de référence, nous avons examiné de très près Core Web Vitals en essayant de nous assurer que nous obtenons de bons scores pour tous. Nous avons choisi de nous concentrer spécifiquement sur les appareils mobiles lents – avec une vitesse de transfert 3G lente, 400 ms RTT et 400 kbps, juste pour être pessimiste. Il n'est donc pas surprenant que Lighthouse n'ait pas non plus été très satisfait de notre site, fournissant des scores rouges entièrement solides pour les articles les plus lourds, et se plaignant inlassablement du JavaScript, du CSS, des images hors écran inutilisées et de leur taille.

 Une capture d'écran des données de Lighthouse montrant opportunités et économies estimées
Lighthouse n'était pas non plus particulièrement satisfait de la performance de certaines pages. C’est celui qui propose de nombreuses vidéos intégrées. ( Grand aperçu )

Une fois que nous avons eu quelques données devant nous, nous pourrions nous concentrer sur l'optimisation des trois pages d'article les plus lourdes, en mettant l'accent sur les CSS critiques (et non critiques), le bundle JavaScript, tâches longues, chargement de polices Web, changements de mise en page et intégrations tierces. Plus tard, nous réviserons également la base de code pour supprimer l'ancien code et utiliser de nouvelles fonctionnalités de navigateur modernes. Il semblait que beaucoup de travail nous attendait, et en effet nous étions assez occupés pour les mois à venir.

Amélioration de l'ordre des actifs dans le

Ironiquement, la toute première chose que nous avons examinée n'était pas ' t même étroitement lié à toutes les tâches que nous avons identifiées ci-dessus. Dans l'atelier de performance, Harry a passé un temps considérable à expliquer l'ordre des actifs dans le de chaque page, soulignant que fournir rapidement un contenu critique signifie être très stratégique et attentif à la façon dont les actifs sont classés dans le code source.

Maintenant, cela ne devrait pas être une grande révélation que le CSS critique est bénéfique pour les performances Web. Cependant, la différence de l'ordre de tous les autres actifs – indices de ressources, préchargement de polices Web, scripts synchrones et asynchrones, CSS complet et métadonnées – a été un peu surprenante.

Nous avons mis tout à l'envers, plaçant le CSS critique avant tous les scripts asynchrones et tous les éléments préchargés tels que les polices, les images, etc. Nous avons décomposé les éléments que nous allons préconnecter ou préchargement par modèle et type de fichier, de sorte que les images critiques, la coloration syntaxique et l'intégration vidéo ne soient demandées tôt que pour un certain type d'articles et de pages.

En général, nous avons soigneusement orchestré l'ordre dans le [19659057]a réduit le nombre d'actifs préchargés qui étaient en concurrence pour la bande passante et s'est concentré sur la bonne exécution des CSS critiques. Si vous souhaitez approfondir certaines des considérations critiques de la commande Harry les met en évidence dans l'article sur CSS et performances du réseau . Ce changement à lui seul nous a apporté environ 3 à 4 points de score Lighthouse dans tous les domaines.

Passer du CSS critique automatisé au CSS critique manuel

Le déplacement des balises était cependant une partie simple de l'histoire. La génération et la gestion des fichiers CSS critiques étaient plus difficiles. En 2017, nous avons créé manuellement des CSS critiques pour chaque modèle, en collectant tous les styles requis pour rendre les premiers 1000 pixels de hauteur sur toutes les largeurs d'écran. Ce fut bien sûr une tâche lourde et peu inspirante, sans parler des problèmes de maintenance pour apprivoiser toute une famille de fichiers CSS critiques et un fichier CSS complet.

Nous avons donc examiné les options sur pour automatiser ce processus comme une partie de la routine de construction. Il n’y avait pas vraiment de pénurie d’outils disponibles. Nous en avons donc testé quelques-uns et avons décidé d’exécuter quelques tests. Nous avons réussi à les configurer et à les exécuter assez rapidement. La sortie semblait être assez bonne pour un processus automatisé, donc après quelques ajustements de configuration, nous l'avons branché et mis en production. Cela s'est produit autour de juillet-août de l'année dernière, ce qui est bien visualisé dans le pic et la baisse des performances dans les données CrUX ci-dessus. Nous avons continué à faire des allers-retours avec la configuration, ayant souvent des problèmes avec des choses simples comme l'ajout de styles particuliers ou la suppression d'autres. Par exemple. styles d'invite de consentement aux cookies qui ne sont pas vraiment inclus sur une page à moins que le script de cookie ne soit initialisé.

En octobre, nous avons apporté des modifications majeures à la mise en page du site, et en examinant le CSS critique, nous avons exécuté dans exactement les mêmes problèmes encore une fois – le résultat généré était assez verbeux et n'était pas tout à fait ce que nous voulions. Ainsi, à titre expérimental fin octobre, nous avons tous regroupé nos forces pour revoir notre approche CSS critique et étudier à quel point un CSS critique artisanal serait beaucoup plus petit. Nous avons pris une profonde inspiration et passé des jours autour de l'outil de couverture de code sur des pages clés. Nous avons regroupé les règles CSS manuellement et supprimé les doublons et le code hérité aux deux endroits – le CSS critique et le CSS principal. C'était un nettoyage indispensable, car de nombreux styles qui ont été écrits en 2017-2018 sont devenus obsolètes au fil des ans.

En conséquence, nous nous sommes retrouvés avec trois fichiers CSS critiques fabriqués à la main, et avec trois autres fichiers qui sont actuellement en cours de travail:

Les fichiers sont insérés dans l'en-tête de chaque template, et pour le moment ils sont dupliqués dans le bundle CSS monolithique qui contient tout ce qui a déjà été utilisé (ou plus vraiment utilisé) sur le site. Pour le moment, nous cherchons à décomposer l'ensemble CSS complet en quelques packages CSS, de sorte qu'un lecteur du magazine ne téléchargerait pas les styles à partir du tableau des emplois ou des pages du livre, mais en atteignant ces pages, il obtiendrait un rendu rapide. avec CSS critique et obtenez le reste du CSS pour cette page de manière asynchrone – uniquement sur cette page.

Certes, les fichiers CSS critiques fabriqués à la main n'étaient pas beaucoup plus petits: nous avons réduit la taille des fichiers CSS critiques d'environ 14% . Cependant, ils ont inclus tout ce dont nous avions besoin dans le bon ordre du début à la fin sans doublons ni styles de remplacement. Cela semblait être un pas dans la bonne direction, et cela nous a donné un coup de pouce pour Phare de 3 à 4 points supplémentaires. Nous progressions.

Modification du chargement de la police Web

Avec l'affichage des polices à portée de main, le chargement des polices semble être un problème dans le passé. Malheureusement, ce n’est pas tout à fait correct dans notre cas. Vous, chers lecteurs, semblez consulter un certain nombre d'articles sur Smashing Magazine. Vous revenez également fréquemment sur le site pour lire un autre article – peut-être quelques heures ou quelques jours plus tard, ou peut-être une semaine plus tard. L'un des problèmes que nous avons rencontrés avec font-display utilisé sur le site était que pour les lecteurs qui se déplaçaient beaucoup entre les articles, nous avons remarqué de nombreux éclairs entre la police de remplacement et la police Web (ce qui ne devrait pas se produirait normalement car les polices seraient correctement mises en cache).

Cela ne semblait pas être une expérience utilisateur décente, nous avons donc examiné les options. Sur Smashing, nous utilisons deux polices principales – Mija pour les titres et Elena pour la copie du corps. Mija est disponible en deux poids (Regular et Bold), tandis qu'Elena vient en trois poids (Regular, Italic, Bold). Nous avons abandonné l’italique gras d’Elena il y a des années lors de la refonte, simplement parce que nous l’avons utilisé sur quelques pages. Nous sous-ensembles les autres polices en supprimant les caractères inutilisés et les plages Unicode.

Nos articles sont principalement définis dans du texte, nous avons donc découvert que la plupart du temps sur le site, la plus grande peinture de contenu est soit le premier paragraphe de texte dans un l'article ou la photo de l'auteur. Cela signifie que nous devons faire très attention à ce que le premier paragraphe apparaisse rapidement dans une police de remplacement, tout en passant gracieusement à la police Web avec un minimum de reflux.

Examinez de près l'expérience de chargement initial de la page d'accueil (ralenti trois fois):

Nous avions quatre objectifs principaux pour trouver une solution:

  1. Lors de la toute première visite, rendre le texte immédiatement avec une police de remplacement;
  2. Faire correspondre les métriques de police des polices de secours et du Web polices pour minimiser les changements de mise en page;
  3. Chargez toutes les polices Web de manière asynchrone et appliquez-les toutes en même temps (1 redistribution au maximum);
  4. Lors des visites suivantes, restituer tout le texte directement dans les polices Web (sans aucun clignotement ni redistribution). [19659077] Au départ, nous avons essayé d'utiliser font-display: swap sur font-face . Cela semblait être l'option la plus simple, cependant, certains lecteurs visiteront un certain nombre de pages, nous nous sommes donc retrouvés avec beaucoup de scintillement avec les six polices que nous rendions sur le site. De plus, avec font-display seul, nous ne pouvions pas regrouper les requêtes ni les repeindre.

    Une autre idée était de tout rendre dans la police de secours lors de la visite initiale puis de demander et de mettre en cache toutes les polices de manière asynchrone, et uniquement lors des visites ultérieures, fournissent des polices Web directement à partir du cache. Le problème avec cette approche était qu'un certain nombre de lecteurs proviennent de moteurs de recherche, et au moins certains d'entre eux ne verront qu'une seule page – et nous ne voulions pas rendre un article dans une seule police système.

    Donc

    Depuis 2017, nous utilisons l'approche Two-Stage-Render pour le chargement de polices Web qui décrit essentiellement deux étapes de rendu: une avec un sous-ensemble minimal de polices Web, et la autre avec une famille complète de poids de police. À l'époque, nous avons créé des sous-ensembles minimaux de Mija Bold et Elena Regular, qui étaient les poids les plus fréquemment utilisés sur le site. Les deux sous-ensembles comprennent uniquement des caractères latins, des signes de ponctuation, des chiffres et quelques caractères spéciaux. Ces polices ( ElenaInitial.woff2 et MijaInitial.woff2 ) étaient de très petite taille – souvent de l'ordre de 10 à 15 Ko. Nous les servons dans la première étape du rendu des polices, affichant la page entière dans ces deux polices.

     CLS causé par le scintillement des polices Web
    CLS causé par le scintillement des polices Web (les ombres sous les images de l'auteur se déplacent en raison du changement de police ). Généré avec Layout Shift GIF Generator . ( Grand aperçu )

    Nous le faisons avec une API de chargement de polices qui nous donne des informations sur les polices chargées et celles qui ne l’ont pas encore été. Dans les coulisses, cela se produit en ajoutant une classe .wf-shared-stage1 au corps avec des styles rendant le contenu dans ces polices:

     .wf-shared-stage1 article,
    Boîte promotionnelle .wf-chargée-stage1,
    Commentaires .wf-shared-stage1 {
        famille de polices: ElenaInitial, sans-serif;
    }
    
    .wf-chargé-stage1 h1,
    .wf-chargé-stage1 h2,
    .wf-chargé-stage1 .btn {
        famille de polices: MijaInitial, sans-serif;
    }
    

    Parce que les fichiers de polices sont assez petits, j'espère qu'ils traverseront le réseau assez rapidement. Alors que le lecteur peut réellement commencer à lire un article, nous chargeons les poids complets des polices de manière asynchrone, et ajoutons .wf-shared-stage2 au corps :

     .wf- article chargé-stage2,
    Boîte promo .wf-chargée-stage2,
    Commentaires .wf-shared-stage2 {
        famille de polices: Elena, sans-serif;
    }
    
    .wf-chargé-stage2 h1,
    .wf-chargé-stage2 h2,
    .wf-chargé-stage2 .btn {
        famille de polices: Mija, sans-serif;
    }
    

    Ainsi, lors du chargement d'une page, les lecteurs obtiendront rapidement un petit sous-ensemble de polices Web, puis nous passerons à la famille de polices complète. Désormais, par défaut, ces commutations entre les polices de secours et les polices Web se produisent de manière aléatoire, en fonction de ce qui arrive en premier sur le réseau. Cela peut sembler assez perturbateur lorsque vous avez commencé à lire un article. Ainsi, au lieu de laisser au navigateur le soin de décider quand changer de police, nous regroupons les repeints, réduisant au minimum l'impact de la redistribution.

     / * Chargement des polices Web avec l'API de chargement des polices pour éviter plusieurs repeints. Avec l'aide d'Irina Lipovaya. * /
    / * Crédit au travail initial de Zach Leatherman: https://noti.st/zachleat/KNaZEg/the-five-whys-of-web-font-loading-performance#sWkN4u4 * /
    
    // Si l'API de chargement de polices est prise en charge ...
    // (Sinon, nous nous en tenons aux polices de secours)
    if ("polices" dans le document) {
    
        // Créer de nouveaux objets FontFace, un pour chaque police
        laissez ElenaRegular = new FontFace (
            "Elena",
            "format url (/fonts/ElenaWebRegular/ElenaWebRegular.woff2) ('woff2')"
        );
        laissez ElenaBold = new FontFace (
            "Elena",
            "format url (/fonts/ElenaWebBold/ElenaWebBold.woff2) ('woff2')",
            {
                poids: "700"
            }
        );
        laissez ElenaItalic = new FontFace (
            "Elena",
            "format url (/fonts/ElenaWebRegularItalic/ElenaWebRegularItalic.woff2) ('woff2')",
            {
                style: "italique"
            }
        );
        laissez MijaBold = new FontFace (
            "Mija",
            "url (/fonts/MijaBold/Mija_Bold-webfont.woff2) format ('woff2')",
            {
                poids: "700"
            }
        );
    
        // Charge toutes les polices mais les rend en même temps
        // s'ils ont été chargés avec succès
        let sharedFonts = Promise.all ([
            ElenaRegular.load(),
            ElenaBold.load(),
            ElenaItalic.load(),
            MijaBold.load()
        ]). then (result => {
            result.forEach (font => document.fonts.add (font));
            document.documentElement.classList.add ('wf-chargé-étape2');
    
            // Utilisé pour les vues répétées
            sessionStorage.foutFontsStage2Loaded = true;
        }). catch (erreur => {
            throw new Error (`Erreur interceptée: $ {error}`);
        });
    
    }
    

    Cependant, que se passe-t-il si le premier petit sous-ensemble de polices ne passe pas rapidement par le réseau? Nous avons remarqué que cela semble se produire plus souvent que nous ne le souhaiterions. Dans ce cas, après l'expiration d'un délai de 3 s, les navigateurs modernes reviennent à une police système (dans notre cas Arial), puis basculent vers ElenaInitial ou MijaInitial juste pour être commuté plus tard à Elena ou Mija, respectivement. Cela a produit un peu trop de flashs lors de notre dégustation. Nous pensions supprimer le premier rendu du premier étage uniquement pour les réseaux lents au départ (via l'API Network Information), mais ensuite nous avons décidé de le supprimer complètement.

    Ainsi, en octobre, nous avons complètement supprimé les sous-ensembles, ainsi que l'étape intermédiaire. Chaque fois que tous les poids des polices Elena et Mija sont téléchargés avec succès par le client et prêts à être appliqués, nous lançons l'étape 2 et repeignons tout en même temps. Et pour rendre les reflux encore moins visibles, nous avons passé un peu de temps à faire correspondre les polices de remplacement et les polices Web . Cela signifiait principalement appliquer des tailles de police et des hauteurs de ligne légèrement différentes pour les éléments peints dans la première partie visible de la page.

    Pour cela, nous avons utilisé font-style-matcher et (ahem, ahem) quelques nombres magiques. C’est aussi la raison pour laquelle nous avons initialement opté pour -apple-system et Arial comme polices de secours globales; San Francisco (rendu via -apple-system) semblait être un peu plus agréable qu'Arial, mais s'il n'est pas disponible, nous avons choisi d'utiliser Arial simplement parce qu'il est largement répandu dans la plupart des systèmes d'exploitation .

    En CSS , cela ressemblerait à ceci:

     .article__summary {
    
        famille de polices: -apple-system, Arial, BlinkMacSystemFont, Roboto Slab, Droid Serif, Segoe UI, Ubuntu, Cantarell, Georgia, sans-serif;
        style de police: italique;
    
        / * Attention: chiffres magiques à venir! * /
        / * San Francisco Italic et Arial Italic ont une hauteur d'x plus grande que Elena * /
        taille de la police: 0,9213em;
        hauteur de ligne: 1,487em;
    }
    
    .wf-chargé-stage2 .article__summary {
        famille de polices: Elena, sans-serif;
        taille de la police: 1em; / * Taille de la police d'origine pour Elena Italic * /
        hauteur de ligne: 1,55em; / * Hauteur de ligne d'origine pour Elena Italic * /
    }
    

    Cela a plutôt bien fonctionné. Nous affichons le texte immédiatement, et les polices Web sont regroupées sur l'écran, provoquant idéalement exactement une redistribution sur la première vue, et aucune redistribution complète sur les vues suivantes.

    Une fois les polices téléchargées, nous les stockons dans un cache de l'agent de service. Lors des visites suivantes, nous vérifions d'abord si les polices sont déjà dans le cache. Si tel est le cas, nous les récupérons du cache du technicien de maintenance et les appliquons immédiatement. Et sinon, on recommence avec le fallback-web-font-switcheroo .

    Cette solution réduit le nombre de redistributions au minimum (un) sur des connexions relativement rapides, tout en conservant les polices de manière persistante et fiable dans le cache. Dans le futur, nous espérons sincèrement remplacer les nombres magiques par f-mods . Peut-être que Zach Leatherman serait fier .

    Identification et démantèlement du JS monolithique

    Lorsque nous avons étudié le fil conducteur du panneau Performance de DevTools, nous savions exactement ce que nous devions faire. Il y avait huit tâches longues qui prenaient entre 70 ms et 580 ms, bloquant l'interface et la rendant non réactive. In general, these were the scripts costing the most:

    • uc.jsa cookie prompt scripting (70ms)
    • style recalculations caused by incoming full.css file (176ms) (the critical CSS doesn’t contain styles below the 1000px height across all viewports)
    • advertising scripts running on load event to manage panels, shopping cart, etc. + style recalculations (276ms)
    • web font switch, style recalculations (290ms)
    • app.js evaluation (580ms)

    We focused on the ones that were most harmful first — so-to-say the longest Long Tasks.

    A screenshot taken from DevTools showing style validations for the smashing magazine front page
    At the bottom, Devtools shows style invalidations — a font switch affected 549 elements that had to be repainted. Not to mention layout shifts it was causing. (Large preview)

    The first one was occurring due to expensive layout recalculations caused by the change of the fonts (from fallback font to web font), causing over 290ms of extra work (on a fast laptop and a fast connection). By removing stage one from the font loading alone, we were able to gain around 80ms back. It wasn’t good enough though because were way beyond the 50ms budget. So we started digging deeper.

    The main reason why recalculations happened was merely because of the huge differences between fallback fonts and web fonts. By matching the line-height and sizes for fallback fonts and web fontswe were able to avoid many situations when a line of text would wrap on a new line in the fallback font, but then get slightly smaller and fit on the previous line, causing major change in the geometry of the entire page, and consequently massive layout shifts. We’ve played with letter-spacing and word-spacing as well, but it didn’t produce good results.

    With these changes, we were able to cut another 50-80ms, but we weren’t able to reduce it below 120ms without displaying the content in a fallback font and display the content in the web font afterwards. Obviously, it should massively affect only first time visitors as consequent page views would be rendered with the fonts retrieved directly from the service worker’s cache, without costly reflows due to the font switch.

    By the way, it’s quite important to notice that in our case, we noticed that most Long Tasks weren’t caused by massive JavaScript, but instead by Layout Recalculations and parsing of the CSS, which meant that we needed to do a bit of CSS cleaning, especially watching out for situations when styles are overwritten. In some way, it was good news because we didn’t have to deal with complex JavaScript issues that much. However, it turned out not to be straightforward as we are still cleaning up the CSS this very day. We were able to remove two Long Tasks for good, but we still have a few outstanding ones and quite a way to go. Fortunately, most of the time we aren’t way above the magical 50ms threshold.

    The much bigger issue was the JavaScript bundle we were serving, occupying the main thread for a whopping 580ms. Most of this time was spent in booting up app.js which contains React, Redux, Lodash, and a Webpack module loader. The only way to improve performance with this massive beast was to break it down into smaller pieces. So we looked into doing just that.

    With Webpack, we’ve split up the monolithic bundle into smaller chunks with code-splittingabout 30Kb per chunk. We did some package.json cleansing and version upgrade for all production dependencies, adjusted the browserlistrc setup to address the two latest browser versions, upgraded to Webpack and Babel to the latest versions, moved to Terser for minification, and used ES2017 (+ browserlistrc) as a target for script compilation.

    We also used BabelEsmPlugin to generate modern versions of existing dependencies. Finally, we’ve added prefetch links to the header for all necessary script chunks and refactored the service worker, migrating to Workbox with Webpack (workbox-webpack-plugin).

    A screenshot showing JavaScript chunks affecting performance with each running no longer than 40ms on the main thread
    JavaScript chunks in action, with each running no longer than 40ms on the main thread. (Large preview)

    Remember when we switched to the new navigation back in mid-2020, just to see a huge performance penalty as a result? The reason for it was quite simple. While in the past the navigation was just static plain HTML and a bit of CSS, with the new navigation, we needed a bit of JavaScript to act on opening and closing of the menu on mobile and on desktop. That was causing rage clicks when you would click on the navigation menu and nothing would happen, and of course, had a penalty cost in Time-To-Interactive scores in Lighthouse.

    We removed the script from the bundle and extracted it as a separate script. Additionally, we did the same thing for other standalone scripts that were used rarely — for syntax highlighting, tables, video embeds and code embeds — and removed them from the main bundle; instead, we granularly load them only when needed.

    Performance stats for the smashing magazine front page showing the function call for nav.js that happened right after a monolithic app.js bundle had been executed
    Notice that the function call for nav.js is happening after a monolithic app.js bundle is executed. That’s not quite right. (Large preview)

    However, what we didn’t notice for months was that although we removed the navigation script from the bundle, it was loading after the entire app.js bundle was evaluated, which wasn’t really helping Time-To-Interactive (see image above). We fixed it by preloading nav.js and deferring it to execute in the order of appearance in the DOM, and managed to save another 100ms with that operation alone. By the end, with everything in place we were able to bring the task to around 220ms.

    A screenshot of the the Long task reduced by almost 200ms
    By prioritizing the nav.js script, we were able to reduce the Long task by almost 200ms. (Large preview)

    We managed to get some improvement in place, but still have quite a way to go, with further React and Webpack optimizations on our to-do list. At the moment we still have two major Long Tasks — font switch (120ms), app.js execution (220ms) and style recalculations due to the size of full CSS (140ms). For us, it means cleaning up and breaking up the monolithic CSS next.

    It’s worth mentioning that these results are really the best-scenario-results. On a given article page we might have a large number of code embeds and video embeds, along with other third-party scripts that would require a separate conversation.

    Dealing With 3rd-Parties

    Fortunately, our third-party scripts footprint (and the impact of their friends’ fourth-party-scripts) wasn’t huge from the start. But when these third-party scripts accumulated, they would drive performance down significantly. This goes especially for video embedding scriptsbut also syntax highlighting, advertising scripts, promo panels scripts and any external iframe embeds.

    Obviously, we defer all of these scripts to start loading after the DOMContentLoaded event, but once they finally come on stage, they cause quite a bit of work on the main thread. This shows up especially on article pages, which are obviously the vast majority of content on the site.

    The first thing we did was allocating proper space to all assets that are being injected into the DOM after the initial page render. It meant width and height for all advertising images and the styling of code snippets. We found out that because all the scripts were deferred, new styles were invalidating existing styles, causing massive layout shifts for every code snippet that was displayed. We fixed that by adding the necessary styles to the critical CSS on the article pages.

    We’ve re-established a strategy for optimizing images (preferably AVIF or WebP — still work in progress though). All images below the 1000px height threshold are natively lazy-loaded (with ), while the ones on the top are prioritized (). The same goes for all third-party embeds.

    We replaced some dynamic parts with their static counterparts — e.g. while a note about an article saved for offline reading was appearing dynamically after the article was added to the service worker’s cache, now it appears statically as we are, well, a bit optimistic and expect it to be happening in all modern browsers.

    As of the moment of writing, we’re preparing facades for code embeds and video embeds as well. Plus, all images that are offscreen will get decoding=async attributeso the browser has a free reign over when and how it loads images offscreen, asynchronously and in parallel.

    A screenshot of the main front page of smashing magazine being highlighted by the Diagnostics CSS tool for each image that does not have a width/height attribute
    Diagnostics CSS in use: highlighting images that don’t have width/height attributes, or are served in legacy formats. (Large preview)

    To ensure that our images always include width and height attributes, we’ve also modified Harry Roberts’ snippet and Tim Kadlec’s diagnostics CSS to highlight whenever an image isn’t served properly. It’s used in development and editing but obviously not in production.

    One technique that we used frequently to track what exactly is happening as the page is being loaded, was slow-motion loading.

    First, we’ve added a simple line of code to the diagnostics CSS, which provides a noticeable outline for all elements on the page.

    * {
      outline: 3px solid red
      }
    
    
    A screenshot of an article published on smashing magazine with red lines on the layout to help check the stability and rendering on the page
    A quick trick to check the stability of the layout, by adding * { outline: 3px red; } and observing the boxes as the browser is rendering the page. (Large preview)

    Then we record a video of the page loaded on a slow and fast connection. Then we rewatch the video by slowing down the playback and moving back and forward to identify where massive layout shifts happen.

    Here’s the recording of a page being loaded on a fast connection:

    Recording for the loading of the page with an outline applied, to observe layout shifts.

    And here’s the recording of a recording being played to study what happens with the layout:

    Auditing the layout shifts by rewatching a recording of the site loading in slow motion, watching out for height and width of content blocks, and layout shifts.

    By auditing the layout shifts this way, we were able to quite quickly notice what’s not quite right on the page, and where massive recalculation costs are happening. As you probably have noticed, adjusting the line-height and font-size on headings might go a long way to avoid large shifts.

    With these simple changes alone, we were able to boost performance score by a whopping 25 Lighthouse points for the video-heaviest articleand gain a few points for code embeds.

    Enhancing The Experience

    We’ve tried to be quite strategic in pretty much everything from loading web fonts to serving critical CSS. However, we’ve done our best to use some of the new technologies that have become available last year.

    We are planning on using AVIF by default to serve images on SmashingMag, but we aren’t quite there yet, as many of our images are served from Cloudinary (which already has beta support for AVIF), but many are directly from our CDN yet we don’t really have a logic in place just yet to generate AVIFs on the fly. That would need to be a manual process for now.

    We’re lazy rendering some of the offset components of the page with content-visibility: auto. For example, the footer, the comments section, as well as the panels way below the first 1000px height, are all rendered later after the visible portion of each page has been rendered.

    We’ve played a bit with link rel="prefetch" and even link rel="prerender" (NoPush prefetch) some parts of the page that are very likely to be used for further navigation — for example, to prefetch assets for the first articles on the front page (still in discussion).

    We also preload author images to reduce the Largest Contentful Paint, and some key assets that are used on each page, such as dancing cat images (for the navigation) and shadow used for all author images. However, all of them are preloaded only if a reader happens to be on a larger screen (>800px), although we are looking into using Network Information API instead to be more accurate.

    We’ve also reduced the size of full CSS and all critical CSS files by removing legacy code, refactoring a number of components, and removing the text-shadow trick that we were using to achieve perfect underlines with a combination of text-decoration-skip-ink and text-decoration-thickness (finally!).

    Work To Be Done

    We’ve spent a quite significant amount of time working around all the minor and major changes on the site. We’ve noticed quite significant improvements on desktop and a quite noticeable boost on mobile. At the moment of writing, our articles are scoring on average between 90 and 100 Lighthouse score on desktop, and around 65-80 on mobile.

    Lighthouse score on desktop shows between 90 and 100
    Performance score on desktop. The homepage is already heavily optimized. (Large preview)
    Lighthouse score on mobile shows between 65 and 80
    On mobile, we hardly ever reach a Lighthouse score above 85. The main issues are still Time to Interactive and Total Blocking Time. (Large preview)

    The reason for the poor score on mobile is clearly poor Time to Interactive and poor Total Blocking time due to the booting of the app and the size of the full CSS file. So there is still some work to be done there.

    As for the next steps, we are currently looking into further reducing the size of the CSSand specifically break it down into modules, similarly to JavaScript, loading some parts of the CSS (e.g. checkout or job board or books/eBooks) only when needed.

    We also explore options of further bundling experimentation on mobile to reduce the performance impact of the app.js although it seems to be non-trivial at the moment. Finally, we’ll be looking into alternatives to our cookie prompt solution, rebuilding our containers with CSS clamp()replacing the padding-bottom ratio technique with aspect-ratio and looking into serving as many images as possible in AVIF.

    That’s It, Folks!

    Hopefully, this little case-study will be useful to you, and perhaps there are one or two techniques that you might be able to apply to your project right away. In the end, performance is all about a sum of all the fine little details, that, when adding up, make or break your customer’s experience.

    While we are very committed to getting better at performance, we also work on improving accessibility and the content of the site.

    So if you spot anything that’s not quite right or anything that we could do to further improve Smashing Magazine, please let us know in the comments to this article!

    Also, if you’d like to stay updated on articles like this one, please subscribe to our email newsletter for friendly web tips, goodies, tools and articles, and a seasonal selection of Smashing cats.

    Smashing Editorial" width="35" height="46" loading="lazy" decoding="async(il)






Source link