Comment nous avons amélioré les performances de SmashingMag

Publié: 2022-03-10
Résumé rapide ↬ Dans cet article, nous examinerons 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 performance Web est similaire, n'est-ce pas ? Cela commence toujours par la refonte tant attendue du site Web. Un jour où un projet, entièrement peaufiné et soigneusement optimisé, est lancé, se classant haut et dépassant les scores de performance dans Lighthouse et WebPageTest. Il y a une célébration et un sentiment d'accomplissement sincère qui règne dans l'air - magnifiquement reflété dans les retweets, les commentaires, les newsletters et les fils de discussion Slack.

Pourtant, au fil du temps, l'excitation s'estompe lentement et des ajustements urgents, des fonctionnalités indispensables et de nouvelles exigences commerciales s'infiltrent. Et soudain, avant que vous ne vous en rendiez compte, la base de code devient un peu trop lourde et fragmentée . les scripts doivent se charger juste un peu plus tôt, et un nouveau contenu dynamique brillant trouve son chemin dans le DOM par les portes dérobées des scripts de quatrième partie et de leurs invités non invités.

Nous y sommes aussi allés à 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 nombreuses casquettes différentes un jour donné. Bien que la performance soit notre objectif depuis près d'une décennie maintenant, nous n'avons jamais vraiment eu d'équipe dédiée à la performance.

Après la dernière refonte fin 2017, c'était Ilya Pukhalski du côté JavaScript (à temps partiel), Michael Riethmueller du côté CSS (quelques heures par semaine), et votre serviteur, jouant à des jeux d'esprit avec CSS critique et essayer de jongler avec un peu trop de choses.

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

En l'occurrence, nous avons perdu la trace des performances dans l'agitation de la routine quotidienne. Nous concevions et construisions des choses, configurions de nouveaux produits, refactorisions les composants et publiions des articles. Ainsi, à la fin de 2020, les choses sont devenues un peu incontrôlables, avec des scores de phare rouge jaunâtre apparaissant lentement dans tous les domaines. Nous avons dû régler cela.

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, les fichiers Sass compilés en CSS, JavaScript divisé en morceaux avec Webpack et Hugo construisant des pages statiques que nous servons ensuite directement à partir d'un Edge CDN. En 2017, nous avons construit l'intégralité 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 une amélioration progressive à l'esprit, ce qui signifie que vous, cher lecteur, pouvez lire chaque article Smashing dans son intégralité sans avoir besoin de démarrer l'application du tout. Ce n'est pas très surprenant non plus - au final, un article publié ne change pas beaucoup au fil des ans, tandis que des éléments dynamiques tels que l'authentification des membres et le paiement ont besoin de l'application pour fonctionner.

La construction complète pour déployer environ 2500 articles en direct prend environ 6 minutes pour le moment. Le processus de construction à lui seul est également devenu une véritable bête au fil du temps, avec des injections CSS critiques, la division du code de Webpack, des insertions dynamiques de panneaux publicitaires et de fonctionnalités, la (re) génération RSS et d'éventuels tests A/B en périphérie.

Début 2020, nous avons commencé la grande refactorisation des composants de mise en page CSS. Nous n'avons jamais utilisé CSS-in-JS ou des composants stylés, mais plutôt un bon vieux système basé sur des composants de modules Sass qui seraient compilés en CSS. En 2017, toute la mise en page a été construite avec Flexbox et reconstruite avec CSS Grid et CSS Custom Properties à la mi-2019. Cependant, certaines pages nécessitaient un traitement particulier en raison de nouveaux spots publicitaires et de nouveaux panneaux produits. Ainsi, même si la mise en page fonctionnait, elle ne fonctionnait pas très bien et était assez difficile à entretenir.

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 un CSS d'abord utilitaire, mais nous n'avons jamais atteint le point où il a été utilisé de manière cohérente sur l'ensemble du site.

Le problème le plus important était le gros paquet JavaScript qui, sans surprise, bloquait le thread principal pendant des centaines de millisecondes. Un gros paquet JavaScript peut sembler déplacé dans un magazine qui ne fait que publier des articles, mais en fait, il y a beaucoup de scripts qui se passent dans les 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 mise en page perturbateurs, et il en va de même pour les panneaux de produits natifs qui mettent en valeur nos produits. Plus un agent de service 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 arriver à un moment donné, 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 garder un œil attentif sur les performances (et nous avions quelques autres choses à garder à l'esprit pour 2020). Le tournant est venu de façon inattendue. Harry Roberts a dirigé son (excellent) Web Performance Masterclass sous forme d'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 ainsi que des outils et des directives 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 d'environ 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 scores particuliers pour comprendre nos performances, mais trop souvent, les scores individuels ne fournissent pas une image complète. Comme David East l'a noté avec éloquence dans son article, la performance Web n'est pas une valeur unique ; c'est une répartition. Même si une expérience Web est fortement et complètement une performance globale optimisée, elle ne peut pas être simplement rapide. Il peut être rapide pour certains visiteurs, mais en fin de compte, il sera également plus lent (ou lent) pour d'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 appareils à travers le monde. Le plus souvent, nous ne pouvons pas vraiment influencer ces choses, nous devons donc nous assurer que notre expérience les adapte à la place.

Essentiellement, notre travail consiste alors à augmenter la proportion d'expériences vives et à diminuer 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 les outils de surveillance des performances fourniront ces données en cas de besoin, mais nous avons examiné spécifiquement 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 concerne Core Web Vitals que Google a annoncé en 2020, et qui contribue également et est exposée dans Lighthouse.

Les plus grandes statistiques de Contentful Paint (LCP) montrant une baisse massive des performances entre mai et septembre 2020
La distribution des performances pour la plus grande peinture de contenu en 2020. Entre mai et septembre, les performances ont chuté massivement. Données de CrUX. ( Grand aperçu )

Nous avons constaté que, dans l'ensemble, nos performances ont fortement régressé tout au long de l'année, avec des baisses particulières vers les mois d'août et de septembre. Une fois que nous avons vu ces graphiques, nous avons pu revenir sur certaines des relations publiques que nous avons diffusées en direct à l'époque pour étudier ce qui s'est réellement passé.

Il n'a pas fallu longtemps pour comprendre qu'à cette époque, nous avons lancé une nouvelle barre de navigation en direct. Cette barre de navigation - utilisée sur toutes les pages - s'appuyait sur JavaScript pour afficher les éléments de navigation dans un menu au toucher ou au clic, mais la partie JavaScript était en fait regroupé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 fichier CSS critique (obsolète) créé manuellement à un système automatisé qui générait des CSS critiques pour chaque modèle - page d'accueil, article, page de produit, événement, tableau d'affichage, etc. - et des CSS critiques en ligne pendant le temps de construction. Pourtant, nous n'avions pas vraiment réalisé à quel point le CSS critique généré automatiquement était plus lourd. Nous devions l'explorer plus en détail.

Et à peu près au même moment, nous ajustions le chargement des polices Web , en essayant de pousser les polices Web de manière plus agressive avec des conseils de ressources tels que le préchargement. Cela semble toutefois contrecarrer nos efforts en matière de performances, car les polices Web retardaient le rendu du contenu, étant prioritaires à côté du fichier CSS complet.

Maintenant, l'une des raisons courantes de la régression est le coût élevé de JavaScript, nous avons donc également examiné Webpack Bundle Analyzer et la carte de demande de Simon Hearne pour obtenir une image visuelle de nos dépendances JavaScript. Il avait l'air plutôt sain au départ.

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

Quelques demandes arrivaient au CDN, un service de consentement aux cookies Cookiebot, Google Analytics, ainsi que nos services internes pour servir des panneaux de produits et de la 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 sur les performances, il est courant d'examiner les performances de certaines pages critiques - très probablement la page d'accueil et très probablement quelques pages d'articles/produits. Cependant, bien qu'il n'y ait qu'une seule page d'accueil, il peut y avoir de nombreuses 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 d'articles lourds en code et en design sur SmashingMag, au fil des ans, nous avons accumulé littéralement des milliers d'articles contenant des GIF lourds, des extraits de code à syntaxe surlignée, des intégrations CodePen, vidéo/audio des intégrations et des fils imbriqués de commentaires sans fin.

Une fois réunis, beaucoup d'entre eux ne provoquaient rien de moins qu'une explosion de la taille du DOM ainsi qu'un travail excessif sur le 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 de style et de repeints - également des tâches coûteuses qui peuvent produire de longues tâches.

Tout cela n'apparaissait pas dans la carte 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 avions - la toute-puissante page d'accueil, la plus longue, celle avec de nombreuses intégrations vidéo et celle avec de nombreuses intégrations CodePen - et avons décidé de les optimiser autant que possible. Après tout, si elles sont rapides, les pages avec une seule intégration CodePen devraient également être plus rapides.

Avec ces pages à l'esprit, la carte était un peu différente. Notez l'énorme ligne épaisse en direction du lecteur Vimeo et du 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 beaucoup de vidéos et/ou d'intégrations vidéo
Sur certaines pages d'articles, le graphique était différent. Surtout avec beaucoup de code ou d'intégrations vidéo, les performances baissaient de manière assez significative. Malheureusement, beaucoup de nos articles en contiennent. ( Grand aperçu )

Pour étudier l'impact sur le thread principal, nous avons approfondi le panneau Performances de DevTools. Plus précisément, nous recherchions des tâches qui durent plus de 50 ms (surlignées par un rectangle rouge 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 exploitables pour savoir par où commencer. Par exemple, nous avons rapidement découvert que le chargement de nos polices Web entraînait un coût de repeinture important, alors que les blocs JavaScript étaient encore suffisamment lourds pour bloquer le thread principal.

Une capture d'écran du panneau de performances dans DevTools montrant des morceaux de JavaScript qui étaient encore assez lourds pour bloquer le thread principal
Étudier le 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 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 chacun d'entre eux. Nous avons choisi de nous concentrer spécifiquement sur les appareils mobiles lents - avec une 3G lente, un RTT de 400 ms et une vitesse de transfert de 400 kbps, juste pour être du côté pessimiste des choses. 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 inutilisé, du CSS, des images hors écran et de leurs tailles.

Une capture d'écran des données Lighthouse montrant les opportunités et les économies estimées
Lighthouse n'était pas non plus particulièrement satisfait des performances de certaines pages. C'est celui avec beaucoup d'intégrations vidéo. ( Grand aperçu )

Une fois que nous avions quelques données devant nous, nous pouvions nous concentrer sur l'optimisation des trois pages d'articles les plus lourdes, en mettant l'accent sur les CSS critiques (et non critiques), le bundle JavaScript, les tâches longues, le chargement des polices Web, les changements de mise en page et les tiers. -intègre. Plus tard, nous réviserons également la base de code pour supprimer le code hérité et utiliser de nouvelles fonctionnalités de navigateur modernes. Cela semblait être beaucoup de travail devant nous, et en effet nous étions très occupés pour les mois à venir.

Améliorer l'ordre des actifs dans le <head>

Ironiquement, la toute première chose que nous avons examinée n'était même pas étroitement liée à 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 <head> de chaque page, soulignant que pour fournir rapidement un contenu critique, il faut être très stratégique et attentif à la façon dont les actifs sont ordonné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, il a été un peu surprenant de voir à quel point l'ordre de tous les autres éléments diffère - conseils de ressources, préchargement des polices Web, scripts synchrones et asynchrones, CSS complet et métadonnées -.

Nous avons bouleversé l'intégralité de <head> , en plaçant les CSS critiques 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 auxquels nous allons nous préconnecter ou précharger par modèle et type de fichier, de sorte que les images critiques, la coloration syntaxique et les intégrations vidéo ne seront demandées au début que pour un certain type d'articles et de pages.

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

Passer d'un CSS critique automatisé à un CSS critique manuel

Déplacer les balises <head> n'était cependant qu'une simple partie de l'histoire. Un plus difficile était la génération et la gestion des fichiers CSS critiques. En 2017, nous avons créé manuellement des CSS critiques pour chaque modèle, en collectant tous les styles nécessaires pour rendre les 1000 premiers pixels de hauteur sur toutes les largeurs d'écran. C'était 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 d' automatisation de ce processus dans le cadre 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 décidé d'effectuer quelques tests. Nous avons réussi à les mettre en place et à les faire fonctionner assez rapidement. La sortie semblait être assez bonne pour un processus automatisé, donc après quelques ajustements de configuration, nous l'avons branché et poussé en production. Cela s'est produit vers 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, les styles d'invite de consentement aux cookies qui ne sont pas vraiment inclus sur une page à moins que le script de cookie n'ait été initialisé.

En octobre, nous avons apporté des modifications majeures à la mise en page du site, et en examinant le CSS critique, nous avons encore une fois rencontré exactement les mêmes problèmes - 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 plus petit. Nous avons pris une profonde inspiration et passé des jours autour de l'outil de couverture de code sur les pages clés. Nous avons regroupé manuellement les règles CSS et supprimé les doublons et le code hérité aux deux endroits : le CSS critique et le CSS principal. C'était en effet un nettoyage indispensable, car de nombreux styles é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 :

  • critique-homepage-manual.css (8,2 Ko, brotlifié)
  • critique-article-manuel.css (8 Ko, brotlifié)
  • articles-critiques-manuel.css (6 Ko, brotlifié)
  • critic-books-manual.css ( travail à faire )
  • critic-events-manual.css ( travail à faire )
  • critic-job-board-manual.css ( travail à faire )

Les fichiers sont alignés dans l'en-tête de chaque modèle, 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 envisageons de décomposer l'ensemble CSS complet en quelques packages CSS, afin qu'un lecteur du magazine ne télécharge pas de styles à partir du site d'emploi ou des pages de livre, mais qu'en atteignant ces pages, il obtiendrait un rendu rapide avec le CSS critique et obtenez le reste du CSS pour cette page de manière asynchrone - uniquement sur cette page.

Certes, la taille des fichiers CSS critiques créés à la main n'était pas beaucoup plus petite : nous avons réduit la taille des fichiers CSS critiques d'environ 14 % . Cependant, ils comprenaient tout ce dont nous avions besoin dans le bon ordre, du haut à la fin, sans doublons ni styles prédominants. Cela semblait être un pas dans la bonne direction, et cela nous a donné un coup de pouce phare de 3 à 4 points supplémentaires. Nous faisions des progrès.

Modification du chargement de la police Web

Avec font-display à 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 flashs entre la police de secours et la police Web (ce qui ne devrait normalement pas se produire car les polices seraient correctement mis en cache).

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

Nos articles sont principalement composés de 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 d'un article, soit 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 secours, tout en passant gracieusement à la police Web avec un minimum de refusions.

Examinez attentivement l'expérience de chargement initiale de la page d'accueil (ralentie trois fois) :

Nous avions quatre objectifs principaux lors de la recherche d'une solution :

  1. Lors de la toute première visite, restituez immédiatement le texte avec une police de secours ;
  2. Faites correspondre les métriques de police des polices de secours et des polices Web 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 (max. 1 refusion);
  4. Lors des visites suivantes, rendez tout le texte directement dans les polices Web (sans clignotement ni refusion).

Au départ, nous avons en fait essayé d'utiliser font-display: swap on font-face . Cela semblait être l'option la plus simple, cependant, comme mentionné ci-dessus, 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 tout le site. De plus, avec font-display seul, nous ne pouvions pas regrouper les demandes ou les repeints.

Une autre idée était de tout afficher dans une 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 suivantes, de fournir des polices Web directement à partir du cache. Le problème avec cette approche était qu'un certain nombre de lecteurs provenaient de moteurs de recherche, et au moins certains d'entre eux ne verraient qu'une seule page - et nous ne voulions pas afficher un article dans une police système uniquement.

Alors qu'est-ce qu'il y a alors ?

Depuis 2017, nous utilisons l'approche Two-Stage-Render pour le chargement des polices Web qui décrit essentiellement deux étapes de rendu : l'une avec un sous-ensemble minimal de polices Web et l'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 incluent 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 d'environ 10 à 15 Ko. Nous les servons dans la première étape du rendu des polices, en 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 qui ont été chargées avec succès et celles qui ne l'ont pas encore été. Dans les coulisses, cela se produit en ajoutant une classe .wf-loaded-stage1 au body , avec des styles rendant le contenu dans ces polices :

 .wf-loaded-stage1 article, .wf-loaded-stage1 promo-box, .wf-loaded-stage1 comments { font-family: ElenaInitial,sans-serif; } .wf-loaded-stage1 h1, .wf-loaded-stage1 h2, .wf-loaded-stage1 .btn { font-family: MijaInitial,sans-serif; }

Étant donné que les fichiers de polices sont assez petits, nous espérons qu'ils passeront assez rapidement sur le réseau. Ensuite, comme le lecteur peut réellement commencer à lire un article, nous chargeons les poids complets des polices de manière asynchrone et ajoutons .wf-loaded-stage2 au corps :

 .wf-loaded-stage2 article, .wf-loaded-stage2 promo-box, .wf-loaded-stage2 comments { font-family: Elena,sans-serif; } .wf-loaded-stage2 h1, .wf-loaded-stage2 h2, .wf-loaded-stage2 .btn { font-family: 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 basculements 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 repaints , réduisant ainsi l'impact de la redistribution au minimum.

 /* Loading web fonts with Font Loading API to avoid multiple repaints. With help by Irina Lipovaya. */ /* Credit to initial work by Zach Leatherman: https://noti.st/zachleat/KNaZEg/the-five-whys-of-web-font-loading-performance#sWkN4u4 */ // If the Font Loading API is supported... // (If not, we stick to fallback fonts) if ("fonts" in document) { // Create new FontFace objects, one for each font let ElenaRegular = new FontFace( "Elena", "url(/fonts/ElenaWebRegular/ElenaWebRegular.woff2) format('woff2')" ); let ElenaBold = new FontFace( "Elena", "url(/fonts/ElenaWebBold/ElenaWebBold.woff2) format('woff2')", { weight: "700" } ); let ElenaItalic = new FontFace( "Elena", "url(/fonts/ElenaWebRegularItalic/ElenaWebRegularItalic.woff2) format('woff2')", { style: "italic" } ); let MijaBold = new FontFace( "Mija", "url(/fonts/MijaBold/Mija_Bold-webfont.woff2) format('woff2')", { weight: "700" } ); // Load all the fonts but render them at once // if they have successfully loaded let loadedFonts = Promise.all([ ElenaRegular.load(), ElenaBold.load(), ElenaItalic.load(), MijaBold.load() ]).then(result => { result.forEach(font => document.fonts.add(font)); document.documentElement.classList.add('wf-loaded-stage2'); // Used for repeat views sessionStorage.foutFontsStage2Loaded = true; }).catch(error => { throw new Error(`Error caught: ${error}`); }); }

Cependant, que se passe-t-il si le premier petit sous-ensemble de polices n'arrive pas rapidement sur le réseau ? Nous avons remarqué que cela semble se produire plus souvent que nous le souhaiterions. Dans ce cas, après l'expiration d'un délai de 3 secondes, les navigateurs modernes retombent sur une police système (dans notre pile de polices, ce serait Arial), puis basculent vers ElenaInitial ou MijaInitial , juste pour être basculés vers Elena ou Mija respectivement plus tard . Cela a produit juste un peu trop de flash lors de notre dégustation. Nous pensions initialement supprimer le rendu de la première étape uniquement pour les réseaux lents (via l'API Network Information), mais nous avons ensuite 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 redistributions encore moins perceptibles, nous avons passé un peu de temps à faire correspondre les polices de secours 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 sur la plupart des systèmes d'exploitation.

En CSS, cela ressemblerait à ceci :

 .article__summary { font-family: -apple-system,Arial,BlinkMacSystemFont,Roboto Slab,Droid Serif,Segoe UI,Ubuntu,Cantarell,Georgia,sans-serif; font-style: italic; /* Warning: magic numbers ahead! */ /* San Francisco Italic and Arial Italic have larger x-height, compared to Elena */ font-size: 0.9213em; line-height: 1.487em; } .wf-loaded-stage2 .article__summary { font-family: Elena,sans-serif; font-size: 1em; /* Original font-size for Elena Italic */ line-height: 1.55em; /* Original line-height for Elena Italic */ }

Cela a assez bien fonctionné. Nous affichons le texte immédiatement et les polices Web apparaissent groupées sur l'écran, provoquant idéalement un redistribution sur la première vue, et aucune redistribution sur les vues suivantes.

Une fois les polices téléchargées, nous les stockons dans le cache d'un service worker. 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 dans le cache du service worker et les appliquons immédiatement. Et sinon, on recommence avec le fallback-web-font-switcheroo .

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

Identifier et décomposer le JS monolithique

Lorsque nous avons étudié le fil conducteur dans le panneau Performances 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. En général, ce sont les scripts qui coûtent le plus cher :

  • uc.js , un script d'invite de cookie (70 ms)
  • recalculs de style causés par le fichier full.css entrant (176 ms) (le CSS critique ne contient pas de styles en dessous de la hauteur de 1000 pixels dans toutes les fenêtres)
  • scripts publicitaires s'exécutant sur événement de chargement pour gérer les panneaux, le panier d'achat, etc. + recalculs de style (276 ms)
  • changement de police Web, recalculs de style (290 ms)
  • évaluation app.js (580ms)

Nous nous sommes concentrés sur ceux qui étaient les plus nocifs en premier - pour ainsi dire les tâches longues les plus longues.

Une capture d'écran tirée de DevTools montrant les validations de style pour la première page du magazine
En bas, Devtools affiche les invalidations de style - un changement de police a affecté 549 éléments qui ont dû être repeints. Sans parler des changements de mise en page que cela provoquait. ( Grand aperçu )

Le premier se produisait en raison de recalculs de mise en page coûteux causés par le changement des polices (de la police de secours à la police Web), entraînant plus de 290 ms de travail supplémentaire (sur un ordinateur portable rapide et une connexion rapide). En supprimant la première étape du chargement des polices uniquement, nous avons pu gagner environ 80 ms en arrière. Ce n'était pas assez bon car ils dépassaient largement le budget de 50 ms. Nous avons donc commencé à creuser plus profondément.

La raison principale pour laquelle les recalculs ont eu lieu était simplement à cause des énormes différences entre les polices de secours et les polices Web. En faisant correspondre la hauteur de ligne et les tailles des polices de secours et des polices Web , nous avons pu éviter de nombreuses situations où une ligne de texte s'enroulerait sur une nouvelle ligne dans la police de secours, mais deviendrait ensuite légèrement plus petite et s'adapterait à la ligne précédente, provoquant un changement majeur dans la géométrie de la page entière, et par conséquent des changements de mise en page massifs. Nous avons également joué avec letter-spacing word-spacing , mais cela n'a pas donné de bons résultats.

Avec ces changements, nous avons pu réduire encore 50 à 80 ms, mais nous n'avons pas pu le réduire en dessous de 120 ms sans afficher le contenu dans une police de secours et afficher ensuite le contenu dans la police Web. De toute évidence, cela ne devrait affecter massivement que les visiteurs pour la première fois, car les vues de page consécutives seraient rendues avec les polices récupérées directement à partir du cache du technicien de service, sans refusions coûteuses dues au changement de police.

Soit dit en passant, il est assez important de noter que dans notre cas, nous avons remarqué que la plupart des tâches longues n'étaient pas causées par du JavaScript massif, mais plutôt par des recalculs de mise en page et l'analyse du CSS, ce qui signifiait que nous devions faire un peu de 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-splitting , about 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. ( Grand aperçu )

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. ( Grand aperçu )

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. ( Grand aperçu )

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 three 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 and customer's browser extensions 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 scripts , but 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 <img loading=lazy> ), while the ones on the top are prioritized ( <img loading=eager> ). The same goes for all third-party embeds.

We replaced some dynamic parts with their static counterparts — eg 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 attribute, so 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. ( Grand aperçu )

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 }
* { 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. ( Grand aperçu )

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 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 article, and 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 threshold, are all rendered later after the visible portion of each page has been rendered.

Nous avons un peu joué avec link rel="prefetch" et même link rel="prerender" (NoPush prefetch) certaines parties de la page qui sont très susceptibles d'être utilisées pour une navigation ultérieure - par exemple, pour pré-extraire des ressources pour la première articles en première page (toujours en discussion).

Nous préchargeons également les images d'auteur pour réduire la plus grande peinture de contenu et certains éléments clés utilisés sur chaque page, tels que les images de chat dansant (pour la navigation) et l'ombre utilisée pour toutes les images d'auteur. Cependant, tous ne sont préchargés que si un lecteur se trouve sur un écran plus grand (> 800px), bien que nous envisageons d'utiliser l'API Network Information à la place pour être plus précis.

Nous avons également réduit la taille du CSS complet et de tous les fichiers CSS critiques en supprimant le code hérité, en refactorisant un certain nombre de composants et en supprimant l'astuce text-shadow que nous utilisions pour obtenir des soulignements parfaits avec une combinaison de text-decoration-skip -encre et texte-décoration-épaisseur (enfin !).

Travail à faire

Nous avons passé beaucoup de temps à travailler sur tous les changements mineurs et majeurs sur le site. Nous avons remarqué des améliorations assez importantes sur le bureau et une amélioration assez notable sur le mobile. Au moment de la rédaction, nos articles obtiennent en moyenne entre 90 et 100 Lighthouse score sur desktop, et environ 65-80 sur mobile .

Le score Lighthouse sur le bureau affiche entre 90 et 100
Score de performance sur ordinateur. La page d'accueil est déjà fortement optimisée. ( Grand aperçu )
Score phare sur les salons mobiles entre 65 et 80
Sur mobile, on atteint rarement un score Lighthouse supérieur à 85. Les principaux enjeux restent le Time to Interactive et le Total Blocking Time. ( Grand aperçu )

La raison du mauvais score sur mobile est clairement le mauvais temps d'interactivité et le mauvais temps de blocage total en raison du démarrage de l'application et de la taille du fichier CSS complet. Il y a donc encore du travail à faire là-bas.

En ce qui concerne les prochaines étapes, nous envisageons actuellement de réduire davantage la taille du CSS , et de le décomposer spécifiquement en modules, de la même manière que JavaScript, en chargeant certaines parties du CSS (par exemple, la caisse ou le tableau d'affichage ou les livres/eBooks) uniquement lorsque nécessaire.

Nous explorons également des options pour regrouper davantage d'expérimentations sur mobile afin de réduire l'impact sur les performances de l' app.js , bien que cela ne semble pas trivial pour le moment. Enfin, nous examinerons des alternatives à notre solution d'invite de cookies, en reconstruisant nos conteneurs avec CSS clamp() , en remplaçant la technique du rapport d' aspect-ratio bas par un rapport d'aspect et en cherchant à servir autant d'images que possible dans AVIF.

C'est ça, les gens !

J'espère que cette petite étude de cas vous sera utile, et peut-être qu'il y a une ou deux techniques que vous pourrez appliquer immédiatement à votre projet. En fin de compte, la performance est une somme de tous les petits détails qui, une fois additionnés, font ou défont l'expérience de votre client.

Bien que nous soyons très attachés à améliorer nos performances, nous travaillons également à l'amélioration de l' accessibilité et du contenu du site. Donc, si vous repérez quelque chose qui ne va pas ou quoi que ce soit que nous pourrions faire pour améliorer encore Smashing Magazine, veuillez nous le faire savoir dans les commentaires de cet article.

Enfin, si vous souhaitez rester à jour sur des articles comme celui-ci, veuillez vous inscrire à notre newsletter par e-mail pour obtenir des conseils Web conviviaux, des goodies, des outils et des articles, ainsi qu'une sélection saisonnière de Smashing cats.