Amélioration des performances de chargement des pages Next.js et Gatsby grâce à la segmentation précise

Une nouvelle stratégie de fragmentation du webpack dans Next.js et Gatsby limite le code en double pour améliorer les performances de chargement des pages.

Chrome collabore avec des outils et dans l'écosystème Open Source JavaScript. De nouvelles optimisations, pour améliorer les performances de chargement de Next.js et Gatsby Cet article aborde une meilleure stratégie de fragmentation précise qui est désormais fourni par défaut dans les deux frameworks.

Introduction

Comme de nombreux frameworks Web, Next.js et Gatsby utilisent le webpack comme outil principal bundler. Lancement de webpack v3 CommonsChunkPlugin pour permettre modules de sortie partagés entre différents points d'entrée dans un seul (ou quelques) "commons" (ou fragments). Le code partagé peut être téléchargé séparément et stocké tôt dans le cache du navigateur, ce qui peut améliorent les performances de chargement.

Ce modèle est devenu populaire avec de nombreux frameworks d'applications monopages adoptant un point d'entrée et de la configuration du bundle, qui se présentait comme suit:

Configuration de point d'entrée et de groupe commune

Bien que pratique, le concept de regroupement de tout le code de module partagé en un seul bloc a ses limites. Les modules qui ne sont pas partagés dans tous les points d'entrée peuvent être téléchargés pour les routes qui ne les utilisent pas ce qui entraîne le téléchargement de plus de code que nécessaire. Par exemple, lorsque page1 charge le bloc common, il charge le code pour moduleC même si page1 n'utilise pas moduleC. C'est pourquoi, comme quelques autres, webpack v4 a supprimé le plug-in en faveur d'un nouveau un: SplitChunksPlugin.

Fractionnement amélioré

Les paramètres par défaut de SplitChunksPlugin fonctionnent bien pour la plupart des utilisateurs. Plusieurs fragments divisés sont créés en fonction d'un certain nombre de conditions ; pour éviter de récupérer du code en double sur plusieurs routes.

Cependant, de nombreux frameworks Web qui utilisent ce plug-in suivent toujours un "Single-Commons" de fragmentation le fractionnement. Next.js, par exemple, générerait un bundle commons contenant tout module utilisée dans plus de 50% des pages et dans toutes les dépendances du framework (react, react-dom, etc.).

const splitChunksConfigs = {
  …
  prod: {
    chunks: 'all',
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: 'commons',
        chunks: 'all',
        minChunks: totalPages > 2 ? totalPages * 0.5 : 2,
      },
      react: {
        name: 'commons',
        chunks: 'all',
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-subscription)[\\/]/,
      },
    },
  },

Bien que le fait d'inclure du code dépendant du framework dans un bloc partagé signifie qu'il peut être téléchargé et mis en cache pour tout point d'entrée, l'heuristique basée sur l'utilisation qui consiste à inclure les modules courants utilisés dans plus de moitié des pages n'est pas très efficace. La modification de ce ratio n'entraînerait que l'un des deux résultats suivants:

  • Si vous réduisez ce ratio, davantage de code inutile est téléchargé.
  • Si vous augmentez le ratio, une plus grande quantité de code est dupliquée pour plusieurs itinéraires.

Pour résoudre ce problème, Next.js a adopté un autre pour SplitChunksPlugin qui réduit du code inutile pour n’importe quelle route.

  • Tout module tiers suffisamment volumineux (supérieur à 160 Ko) est divisé en un module distinct fragment
  • Un bloc frameworks distinct est créé pour les dépendances du framework (react, react-dom et etc.)
  • Autant de fragments partagés que nécessaire sont créés (jusqu'à 25)
  • La taille minimale d'un fragment à générer est fixée à 20 Ko.

Cette stratégie de fragmentation précise offre les avantages suivants:

  • Les temps de chargement des pages sont réduits. Émettre plusieurs fragments partagés, au lieu d'un seul, réduit la quantité de code inutile (ou en double) pour chaque point d'entrée.
  • Amélioration de la mise en cache lors des navigations. Diviser les bibliothèques volumineuses et les dépendances de framework en fragments distincts réduit le risque d'invalidation du cache, car il est peu probable modifier jusqu'à ce qu'une mise à niveau soit effectuée.

Vous pouvez voir l'intégralité de la configuration adoptée par Next.js dans webpack-config.ts.

Autres requêtes HTTP

SplitChunksPlugin a défini la base d'une fragmentation précise, en appliquant cette approche à une comme Next.js, n'était pas un concept entièrement nouveau. Toutefois, de nombreux frameworks ont continué à utiliser une seule heuristique et « commons » pour plusieurs raisons. Cela inclut la préoccupation que de nombreuses autres requêtes HTTP peuvent avoir un impact négatif sur les performances du site.

Les navigateurs ne peuvent ouvrir qu'un nombre limité de connexions TCP vers une seule origine (six pour Chrome). réduire le nombre de fragments générés par un bundler permet de garantir que le nombre total de requêtes reste en deçà de ce seuil. Toutefois, cela ne s'applique qu'à HTTP/1.1. Multiplexage en HTTP/2 permet de diffuser plusieurs requêtes en parallèle à l'aide d'une seule connexion sur une même origine. En d'autres termes, nous n'avons généralement pas à nous soucier de limiter le nombre de fragments émises par notre bundler.

Tous les principaux navigateurs sont compatibles avec HTTP/2. Les équipes Chrome et Next.js voulait savoir si l'augmentation du nombre de requêtes en divisant le seul "commons" de Next.js lot en plusieurs fragments partagés affecterait de quelque manière que ce soit les performances de chargement. Elle a commencé par mesurer les performances d'un seul site tout en modifiant le nombre maximal de requêtes parallèles à l'aide du maxInitialRequests .

Chargement des pages avec une augmentation du nombre de requêtes

En moyenne, trois essais portant sur une même page Web ont été exécutés load, start-render et les heures de First Contentful Paint sont toutes restées à peu près les mêmes lorsque la valeur le nombre de requêtes (de 5 à 15). Fait intéressant, nous avons remarqué une légère baisse des performances après avoir divisé de façon agressive des centaines de requêtes.

Performances de chargement des pages avec des centaines de requêtes

Cela a montré que rester en dessous d'un seuil fiable (20 à 25 requêtes) offrait le bon équilibre. entre les performances de chargement et l'efficacité de la mise en cache. Après quelques tests de référence, 25 ont été choisies comme le nombre de maxInitialRequest.

La modification du nombre maximal de requêtes exécutées en parallèle a entraîné la création de plusieurs requêtes. partagé et en les séparant de façon appropriée pour chaque point d'entrée, ce qui a permis de code inutile pour une même page.

Réduction de la charge utile JavaScript grâce à une augmentation de la fragmentation

Ce test visait uniquement à modifier le nombre de demandes pour voir s'il y aurait peut avoir un impact négatif sur les performances de chargement des pages. Les résultats suggèrent de définir maxInitialRequests sur 25 sur la page de test était optimal, car il a réduit la taille de la charge utile JavaScript sans ralentir vers le bas de la page. La quantité totale de JavaScript nécessaire pour hydrater la page est restée ce qui explique pourquoi le chargement des pages ne s'est pas forcément amélioré quantité de code.

webpack utilise une taille minimale de 30 Ko par défaut pour qu'un fragment soit généré. En revanche, associer La valeur maxInitialRequests de 25 avec une taille minimale de 20 Ko a plutôt permis d'améliorer la mise en cache.

Réductions de la taille avec des fragments précis

De nombreux frameworks, y compris Next.js, s'appuient sur le routage côté client (géré par JavaScript) pour injecter des tags de script plus récents pour chaque transition de route. Mais comment prédéterminer ces blocs dynamiques au moment de la compilation ?

Next.js utilise un fichier manifeste de compilation côté serveur pour déterminer les fragments en sortie utilisés par différents points d'entrée. Pour fournir également ces informations au client, une version abrégée côté client fichier manifeste de compilation a été créé pour mapper toutes les dépendances de chaque point d'entrée.

// Returns a promise for the dependencies for a particular route
getDependencies (route) {
  return this.promisedBuildManifest.then(
    man => (man[route] && man[route].map(url => `/_next/${url}`)) || []
  )
}
Sortie de plusieurs fragments partagés dans une application Next.js.

Cette nouvelle stratégie de fragmentation granulaire a d'abord été déployée dans Next.js derrière un indicateur, où elle a été testée sur un le nombre d'utilisateurs de la première heure. Nombre d'entre eux ont constaté une réduction significative de la quantité totale de JavaScript utilisée pour leur tout le site:

Site Web Variation JS totale Différence (en %)
https://www.barnebys.com/ -238 Ko -23%
https://sumup.com/ -220 Ko -30%
https://www.hashicorp.com/ -11 Mo -71%
Réductions de la taille JavaScript pour toutes les routes (compressées)

La version finale a été envoyée par défaut dans la version 9.2.

Gatsby

Gatsby avait l'habitude de suivre la même approche, qui consistait à utiliser heuristique pour définir des modules courants:

config.optimization = {
  …
  splitChunks: {
    name: false,
    chunks: `all`,
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: `commons`,
        chunks: `all`,
        // if a chunk is used more than half the components count,
        // we can assume it's pretty global
        minChunks: componentsCount > 2 ? componentsCount * 0.5 : 2,
      },
      react: {
        name: `commons`,
        chunks: `all`,
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
      },

En optimisant la configuration de son webpack pour adopter une stratégie de fragmentation granulaire similaire, l'entreprise a également a constaté une réduction importante de l'utilisation de JavaScript sur de nombreux sites de grande taille:

Site Web Variation JS totale Différence (en %)
https://www.gatsbyjs.org/ -680 Ko -22%
https://www.thirdandgrove.com/ -390 Ko -25 %
https://ghost.org/ -1,1 Mo -35%
https://reactjs.org/ -80 Ko - 8 %
Réductions de la taille JavaScript pour toutes les routes (compressées)

Consultez les RP pour comprendre implémenté cette logique dans sa configuration webpack, qui est livrée par défaut dans la version 2.20.7.

Conclusion

Le concept d'envoi de fragments granulaires n'est pas spécifique à Next.js, Gatsby ni même Webpack. Tout le monde devraient envisager d'améliorer la stratégie de fragmentation de leur application si elle suit un grand nombre quel que soit le framework ou le bundler utilisé.

  • Si vous souhaitez appliquer les mêmes optimisations de fragmentation à une application vanilla React, consultez cet exemple de code l'application Nest. Elle utilise un simplifiée de la stratégie de fragmentation granulaire et peut vous aider à commencer à appliquer la même une sorte de logique à votre site.
  • Pour le Rollup, les fragments sont créés de manière détaillée par défaut. Jetez un œil à manualChunks si vous souhaitez configurer le comportement.