Layer0 est un contributeur majeur au framework PWA open-source eCommerce, React Storefront. Plus tôt cette année, nous avons contribué de nombreuses nouvelles fonctionnalités et optimisations comme React Storefront v7, Deux des plus importantes ont été le passage à Next.js et la suppression de plusieurs dépendances clés (par exemple MobX) en faveur des nouvelles fonctionnalités intégrées de React pour la gestion de l’état, telles que le hook useState et l’API Context. Cela a entraîné une réduction de moitié de la taille de l’offre groupée du navigateur.
Ce fut un gain agréable à l’époque et a contribué à améliorer les scores de performance de Lighthouse (v5.7) pour les applications React Storefront typiques des années 80 aux années 90, mesurées par PageSpeed Insights (PSI). Pour le contexte, un score de plus de 83 a surpassé 99% des 500 meilleurs sites Web de commerce électronique sur Lighthouse v5.7. Nous ne savions pas à quel point la réduction du bundle s’avérerait essentielle dans les mois à venir, lorsque Lighthouse v6.0 tomberait comme une bombe et anéantirait les scores de performance de tout le monde.
Découvrez comment la distribution des scores Lighthouse mesurés sur PSI pour les principaux sites de commerce électronique a changé lorsque v6.0 a chuté :
Les nouvelles métriques de Lighthouse 6,0 : TBT, LCP et CLS
Lighthouse v6.0 introduit plusieurs nouvelles métriques de vitesse perceptuelle et reformule la façon dont les métriques globales affectent le score de performance Lighthouse d’une page.Phare 5,7 | Poids | Phare 6,0 | Poids |
---|---|---|---|
Peinture de première qualité (FCP) | 20% | Peinture de première qualité (FCP) | 15% |
Indice de vitesse (si) | 27% | Indice de vitesse (si) | 15% |
Première peinture significative (FMP) | 7% | Peinture à teneur élevée (LCP) | 25% |
Premier CPU inactif (FCI) | 13% | Temps de blocage total (TBT) | 15% |
Temps d'interactivité (TTI) | 33% | Temps d'interactivité (TTI) | 15% |
- | - | Décalage de mise en page cumulé (CLS) | 5% |
Temps de blocage total (TBT)
Le temps de blocage total est une nouvelle mesure incluse dans Lighthouse v6.0 qui mesure le temps pendant lequel l’analyse et l’exécution de JavaScript bloque le thread principal pendant le chargement de la page. Cette métrique traite très sévèrement les SPA/PWA modernes et lourds en JavaScript. Même avec la réduction de 50 % de la taille des lots de React Storefront 7, nous avons constaté une baisse de 20 à 30 points du score de performance Lighthouse v6.0 pour les pages produits, en grande partie due à l’inclusion de TBT comme métrique qui influence 25 % du score de performance global.
Si vous utilisez un framework isomorphe, comme Next.js, qui prend en charge le rendu côté serveur, TBT est principalement déterminé par la taille du bundle et le temps d’hydratation. En termes simples, la seule façon d’améliorer TBT est de supprimer les dépendances, d’optimiser vos composants ou de simplifier votre site en utilisant moins de composants.
Peinture de plus grande qualité (LCP)
LCP est une nouvelle mesure qui a un poids de 25% sur le score global Lighthouse v6.0. LCP vise à quantifier comment l’utilisateur perçoit la performance de chargement de page initiale en observant combien de temps le plus grand élément contentful prend pour terminer la peinture. Pour la plupart des sites, en particulier dans les sites de commerce électronique, le plus grand élément contentful est l’image de héros. Dans le cas des pages produit dans les applications React Storefront, l’élément contentful le plus important est l’image principale du produit. Si vous n’êtes pas sûr de quel élément il s’agit sur votre site, PSI vous dira:
Décalage de mise en page cumulé (CLS)
Le décalage de mise en page cumulé mesure le décalage de la mise en page lors du chargement initial de la page. Le décalage de disposition est le plus souvent causé par les images, qui ont tendance à pousser les éléments autour d’elles lorsqu’elles se redimensionnent pour s’adapter à l’image une fois les données téléchargées à partir du réseau. Le décalage de mise en page peut souvent être complètement éliminé en réservant de l’espace pour chaque image avant son chargement. Heureusement, le composant image de React Storefront le fait déjà, de sorte que l’application de démarrage React Storefront affiche un score CLS parfait de 0 prêt à l’emploi. Il convient de noter que d’autres coupables communs de pauvres CLS sont des bannières et des popups qui apparaissent après que la page est initialement peinte. Les utilisateurs les détestent et maintenant Lighthouse le fait aussi.Comment nous avons optimisé React Storefront pour Lighthouse v6.0
Lorsque nous avons testé pour la première fois la page produit de l’application React Storefront Starter sur Lighthouse v6.0, en utilisant PageSpeed Insights, elle a obtenu un score dans les années 60 les plus faibles :Pour améliorer le score, nous avons d’abord jeté notre dévolu sur LCP. À 2,5 secondes, le FCP était extrêmement élevé (nous y reviendrons plus tard), mais l’écart de près de 3 secondes entre le FCP et le LCP se démarquait comme quelque chose qui avait besoin d’être amélioré.
Optimisation de LCP
L’image principale du produit de React Storefront (simplement un espace réservé – la boîte verte avec « Product 1 » au centre) a été rendue sous forme de balise HTML img avec une URL src pointant vers https://via.placeholder.com. Ce site a un TTFB décent (environ 150 ms), qui pourrait être amélioré en passant à un CDN plus rapide comme Layer0 CDN-as-JavaScript. Pourtant, nous doutons que 150 ms expliquent l’écart de près de 3 secondes entre le FCP et le LCP. Pour éliminer l’écart, nous avons inséré l’image en utilisant une URL de données base64 comme celle-ci :
Nous l’avons fait en téléchargeant l’image principale, en la convertissant en base64, et en la remplissant dans l’attribut src pendant le rendu côté serveur sur le cloud JavaScript Serverless de Layer0. Voici la ligne dans le point de terminaison API du produit:
const mainProductImage = result.product.media.full[0]
mainProductImage.src = await getBase64ForImage(mainProductImage.src)
Et voici le code qui récupère et convertit l’image en base 64:
import fetch from 'node-fetch'
export default async function getBase64ForImage(src) {
const res = await fetch(src)
const contentType = res.headers.get('content-type')
const buffer = await res.buffer()
return `data:${contentType};base64,${buffer.toString('base64')}`
return `data:${contentType};base64,${buffer.toString('base64')}` return `data:${contentType};base64,${buffer.toString('base64')}` return `data:${contentType};base64,${buffer.toString('base64')}`
}
Assez simple et old School. Et l’impact sur le score ?
En faisant tomber le LCP de 5.3s à 2.8s, nous avons gagné 21 points dans le score Lighthouse v6.0 de la page ! Il est un peu troublant de voir comment un si petit changement peut affecter considérablement Lighthouse v6.0 score, mais nous allons le prendre. Il convient de noter que tous les paramètres varient quelque peu entre les séries, mais que le score global était constant dans les années 80 Pour le contexte, le site Web de commerce électronique le plus performant sur la v6.0 note 87 tel que mesuré sur PSI et semble être tout droit sorti des années 90 – jetez un oeil www.rockauto.com
L’écart entre FCP et LCP montré ci-dessus était à peu près aussi grand que nous l’avons vu sur plusieurs passages. La plupart du temps, l’écart se situait dans la plage de 100 ms à 300 ms. Parfois, FCP et LCP étaient identiques.
Optimisation du Service d’orientation pas à pas
Ensuite, nous avons essayé d’améliorer le TBT. C’était assez difficile. Comme mentionné précédemment, pour améliorer le LCP, vous devez réduire la taille de votre bundle JavaScript ou améliorer le temps d’hydratation. Avec la plupart des applications, il n’est tout simplement pas possible de commencer à arracher les dépendances pour rendre le bundle plus petit. Les applications construites avec React Storefront 7 bénéficient déjà de nombreuses optimisations de compilation qui minimisent la taille du bundle fournie par la configuration webpack de Next.js, ainsi que d’optimisations babel spécifiques pour Material UI. Où pourrions-nous améliorer ? Temps d’hydratation.
Hydratation paresseuse pour la victoire!
Heureusement, la communauté React Storefront avait déjà commencé à travailler sur le soutien de l’hydratation paresseuse avant la chute de Lighthouse v6.0. Cela nous a certainement fait accélérer nos efforts.
Dans le cas où vous ne le savez pas, hydratation fait référence à React prenant le contrôle des éléments HTML rendus sur le serveur afin qu’ils deviennent interactifs. Les boutons deviennent cliquables ; les carrousels deviennent rapides, etc Plus une page a de composants, plus l’hydratation prend de temps. Les composants complexes, tels que le menu principal et le carrousel d’image du produit, prennent encore plus de temps.
L’hydratation paresseuse implique de retarder l’hydratation de certains composants jusqu’à ce qu’elle soit absolument nécessaire et, surtout, après le chargement initial de la page (et après le calcul du TBT). Une hydratation paresseuse peut être risquée. Vous devez vous assurer que les éléments de la page sont prêts à répondre à la saisie de l’utilisateur avant que celui-ci ne tente d’interagir avec eux.
La mise en œuvre de Lazy Hydration sur React Storefront s’est avérée assez difficile en raison de la dépendance de Material UI à CSS-in-JS, qui ajoute dynamiquement des styles au document uniquement après l’hydratation des composants. Je vais sauvegarder les détails pour une autre fois. En fin de compte, nous avons construit un composant LazyHydrate que les développeurs peuvent insérer n’importe où dans l’arborescence des composants pour retarder l’hydratation jusqu’à ce qu’un événement spécifique se produise, comme l’élément défilant dans la fenêtre d’affichage ou l’utilisateur touchant l’écran.
Voici un exemple où nous hydratons paresseux le MediaCarousel qui affiche les principales images du produit:
import LazyHydrate from 'react-storefront/LazyHydrate'
Nous avons appliqué une hydratation paresseuse à plusieurs domaines de l’application, notamment:
- Le menu coulissant : nous hydratons cela lorsque l’utilisateur appuie sur le bouton hamburger.
- Toutes les commandes sous le pli : comprennent les sélecteurs de taille, de couleur et de quantité, ainsi que les onglets d’informations sur le produit.
- Le carrousel d’image principal : ce menu et le menu principal sont probablement les composants avec le plus de fonctionnalités et, par conséquent, les plus chers à hydrater.
Voici le score Lighthouse v6.0 avec une hydratation paresseuse appliquée:
L’hydratation paresseuse a réduit le TBT de près de 40% et réduit le TTI (qui a un poids de 15% sur les scores en v6.0) de 700ms. Cela a permis un gain de 6 points dans le score global Lighthouse v6.0.
Vous remarquerez que FCP a augmenté un peu, mais que LCP a baissé. Ces petits changements sont essentiellement « dans le bruit » que vous obtenez lorsque vous exécutez PageSpeed Insights. Tous les scores fluctuent légèrement entre les courses.
Optimisation du FCP
En nous basant sur le score ci-dessus, nous avons estimé que le FCP et/ou le LCP pourraient être encore améliorés. Nous savons que les scripts peuvent bloquer le rendu, nous avons donc regardé comment Next.js importe les scripts dans le document:
Utiliser async ici pourrait ne pas être le meilleur choix. Si le script est téléchargé pendant le rendu, il peut interrompre le rendu pendant l’évaluation du script, ce qui augmente les temps FCP et LCP. L’utilisation de defer au lieu d’async garantirait que les scripts ne sont évalués qu’une fois le document peint.
Malheureusement, Next.js ne vous permet pas de modifier la façon dont les scripts sont importés, nous avons donc dû étendre le composant NextScript comme suit :
import React from 'react'
import { NextScript as OriginalNextScript } from 'next/document'
/**
* A replacement for NextScript from `next/document` that gives you greater control over how script elements are rendered.
* This should be used in the body of `pages/_document.js` in place of `NextScript`.
*/
export default class NextScript extends OriginalNextScript {
static propTypes = {
/**
* Set to `defer` to use `defer` instead of `async` when rendering script elements.
*/
mode: PropTypes.oneOf(['async', 'defer']),
}
static defaultProps = {
mode: 'async',
}
getScripts() {
return super.getScripts().map(script => {
return React.cloneElement(script, {
key: script.props.src,
defer: this.props.mode === 'defer' ? true : undefined,
async: this.props.mode === 'async' ? true : undefined,
})
})
}
}
Ensuite, nous avons ajouté ce qui suit à pages/_document.js:
À notre grand plaisir, cela a permis d’améliorer le LCP et les scores globaux :
Il a également légèrement heurté le FCP sur de nombreuses courses, mais cela peut être dans le «bruit». Néanmoins, le score global était constamment supérieur de 2-3 points en utilisant le report vs async.
En résumé
Lorsque Lighthouse v6.0 a été publié fin mai 2020, les scores de performance de nombreux sites, y compris les applications React Storefront, ont chuté. Avant les optimisations, les performances PDP de l’application de démarrage React Storefront étaient embourbées dans les années 60 Avec ces optimisations, nous l’avons mis dans l’air désormais raréfié des basses années 90 À ce stade, nous pensons que la seule façon d’améliorer encore le score serait de supprimer les dépendances, ce qui pourrait signifier de sacrifier la productivité des développeurs aux performances des applications.
C’est une discussion pour une autre fois. Laissez-moi vous laisser avec certaines choses que nous avons essayées qui n’ont pas fonctionné:
Preact
Preact réduit la taille du bundle de 20 à 30%, mais les scores Lighthouse v6.0 étaient constamment pires sur toutes les métriques, même TTI. Nous n’avons aucune idée de pourquoi, mais nous savons que ce n’est pas nouveau ou exclusif à Lighthouse v6.0. Il était plus lent avec Lighthouse v5.7 également. Nous continuons à vérifier périodiquement et espérons qu’un jour cela sera corrigé.
Tronçonnage
Next.js a récemment introduit un découpage plus fin des ressources de navigateur. Lorsque cela a été introduit pour la première fois dans Next.js 9,1, nous avons remarqué que les petits morceaux supplémentaires ont en fait aggravé le TTI. Cela rend probablement l’application plus rapide pour les utilisateurs qui reviennent après la sortie d’une nouvelle version, car elle peut mieux exploiter le cache du navigateur, mais Lighthouse ne se soucie pas de tout cela. React Storefront a donc limité le nombre de fragments de navigateur à un pendant un certain temps:
const webpack = require('webpack')
const withReactStorefront = require('react-storefront/plugins/withReactStorefront')
module.exports = withReactStorefront({
target: 'serverless',
webpack: config => {
config.plugins.push(
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
})
)
return config
},
})
Polices Web
La plupart des sites utilisent une police Web personnalisée. Par défaut, React Storefront utilise Roboto (bien que cela puisse être modifié ou supprimé). Les polices Web personnalisées tuent les performances, simples et simples. Supprimez la police web et vous gagnerez environ 1 seconde de FCP.
Comme pour les analyses, les parties prenantes semblent disposées à transiger sur les performances pour avoir une police spécifique. React Storefront utilise la police Web à partir de la même origine que le site pour éliminer le temps de négociation TLS que vous auriez à engager si vous aviez chargé la police à partir d’un CDN tiers, tel que Google Fonts. Plus précisément, nous utilisons le paquet typeface-roboto de NPM. Lorsqu’elle est combinée avec le chargeur css de Webpack , l’utilisation de Typeface-roboto entraîne l’importation de la police via un fichier CSS séparé que le navigateur doit télécharger et analyser. Nous pensions que l’intégration de ce CSS dans le document pourrait aider à la performance, mais ce n’était pas le cas.
Quand il s’agit de performance, vous devez toujours mesurer. Les optimisations qui devraient fonctionner en théorie peuvent ne pas être en pratique.
Boostez votre score Lighthouse
Nos clients se classent dans le 95e centile des 500 meilleurs sites de commerce électronique sur Lighthouse v6.0.