Une stratégie de segmentation webpack plus récente dans Next.js et Gatsby réduit le code en double pour améliorer les performances de chargement des pages.
Chrome collabore avec les outils et les frameworks de l'écosystème Open Source JavaScript. Un certain nombre d'optimisations ont été ajoutées récemment pour améliorer les performances de chargement de Next.js et Gatsby. Cet article présente une stratégie de segmentation granulaire améliorée qui est désormais fournie par défaut dans les deux frameworks.
Introduction
Comme de nombreux frameworks Web, Next.js et Gatsby utilisent webpack comme bundler principal. webpack v3 a introduit CommonsChunkPlugin pour permettre de générer des modules partagés entre différents points d'entrée dans un seul (ou plusieurs) fragment(s) "commun(s)". Le code partagé peut être téléchargé séparément et stocké dans le cache du navigateur dès le début, ce qui peut améliorer les performances de chargement.
Ce modèle est devenu populaire auprès de nombreux frameworks d'applications monopages qui ont adopté une configuration de point d'entrée et de bundle semblable à celle-ci :

Bien que pratique, le concept de regroupement de tout le code du module partagé dans un seul fragment présente des limites. Les modules qui ne sont pas partagés dans chaque point 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 d'une quantité de code plus importante que nécessaire. Par exemple, lorsque page1 charge le fragment common, il charge le code de moduleC, même si page1 n'utilise pas moduleC.
Pour cette raison, ainsi que pour d'autres, webpack v4 a supprimé le plug-in au profit d'un nouveau : SplitChunksPlugin.
Amélioration de la segmentation
Les paramètres par défaut de SplitChunksPlugin conviennent à la plupart des utilisateurs. Plusieurs fragments 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 une approche "single-commons" pour la division des fragments. Next.js, par exemple, génère un bundle commons contenant tous les modules utilisés dans plus de 50 % des pages et 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 l'inclusion de code dépendant du framework dans un fragment partagé signifie qu'il peut être téléchargé et mis en cache pour n'importe quel point d'entrée, l'heuristique basée sur l'utilisation consistant à inclure des modules courants utilisés dans plus de la moitié des pages n'est pas très efficace. La modification de ce ratio n'aurait que l'un des deux résultats suivants :
- Si vous réduisez le ratio, davantage de code inutile est téléchargé.
- Si vous augmentez le ratio, davantage de code est dupliqué sur plusieurs routes.
Pour résoudre ce problème, Next.js a adopté une configuration différente pour SplitChunksPlugin qui réduit le code inutile pour n'importe quelle route.
- Tout module tiers suffisamment volumineux (supérieur à 160 Ko) est divisé en un fragment individuel.
- Un fragment
frameworksdistinct est créé pour les dépendances du framework (react,react-dom, etc.). - Autant de fragments partagés que nécessaire sont créés (jusqu'à 25).
- La taille minimale d'un fragment à générer est désormais de 20 Ko.
Cette stratégie de segmentation granulaire offre les avantages suivants :
- Les temps de chargement des pages sont améliorés. L'émission de plusieurs fragments partagés, au lieu d'un seul, réduit la quantité de code inutile (ou en double) pour n'importe quel point d'entrée.
- Amélioration de la mise en cache pendant la navigation. La division des bibliothèques volumineuses et des dépendances de framework en fragments distincts réduit le risque d'invalidation du cache, car il est peu probable que les deux changent avant une mise à niveau.
Vous pouvez consulter l'intégralité de la configuration adoptée par Next.js dans webpack-config.ts.
Plus de requêtes HTTP
SplitChunksPlugin a défini la base du chunking précis, et l'application de cette approche à un framework comme Next.js n'était pas un concept entièrement nouveau. Toutefois, de nombreux frameworks ont continué à utiliser une seule stratégie d'heuristique et de bundle "commons" pour plusieurs raisons. Cela inclut la crainte qu'un nombre beaucoup plus élevé de requêtes HTTP puisse 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). Par conséquent, réduire le nombre de fragments générés par un bundler permet de s'assurer que le nombre total de requêtes reste inférieur à ce seuil. Toutefois, cela n'est vrai que pour HTTP/1.1. Le multiplexage dans HTTP/2 permet de diffuser plusieurs requêtes en parallèle à l'aide d'une seule connexion sur une seule origine. En d'autres termes, nous n'avons généralement pas à nous soucier de limiter le nombre de fragments émis par notre bundler.
Tous les principaux navigateurs sont compatibles avec HTTP/2. Les équipes Chrome et Next.js voulaient voir si l'augmentation du nombre de requêtes en divisant le bundle "commons" unique de Next.js en plusieurs fragments partagés affecterait les performances de chargement. Ils ont commencé par mesurer les performances d'un seul site tout en modifiant le nombre maximal de requêtes parallèles à l'aide de la propriété maxInitialRequests.

En moyenne, sur trois exécutions de plusieurs essais sur une seule page Web, les temps load, start-render et First Contentful Paint sont restés à peu près identiques lorsque le nombre maximal de requêtes initiales variait (de 5 à 15). Fait intéressant, nous n'avons remarqué une légère dégradation des performances qu'après avoir divisé de manière agressive en centaines de requêtes.

Cela a montré que le fait de rester sous un seuil fiable (20 à 25 requêtes) permettait de trouver 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 a été sélectionné comme nombre de maxInitialRequest.
La modification du nombre maximal de requêtes effectuées en parallèle a entraîné la création de plusieurs bundles partagés. En les séparant de manière appropriée pour chaque point d'entrée, la quantité de code inutile pour la même page a été considérablement réduite.

Ce test consistait uniquement à modifier le nombre de requêtes pour voir si cela avait un impact négatif sur les performances de chargement des pages. Les résultats suggèrent que la définition de maxInitialRequests sur 25 sur la page de test était optimale, car elle a réduit la taille de la charge utile JavaScript sans ralentir la page. La quantité totale de code JavaScript nécessaire pour hydrater la page est restée à peu près la même, ce qui explique pourquoi les performances de chargement de page ne se sont pas nécessairement améliorées avec la réduction de la quantité de code.
webpack utilise 30 ko comme taille minimale par défaut pour générer un fragment. Cependant, l'association d'une valeur maxInitialRequests de 25 à une taille minimale de 20 Ko a permis d'améliorer la mise en cache.
Réduction de la taille grâce à 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 de nouveaux tags de script pour chaque transition de route. Mais comment prédéterminer ces fragments dynamiques au moment de la compilation ?
Next.js utilise un fichier manifeste de compilation côté serveur pour déterminer quels fragments de sortie sont utilisés par différents points d'entrée. Pour fournir également ces informations au client, un fichier manifeste de compilation abrégé côté client 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}`)) || []
)
}
Cette nouvelle stratégie de fragmentation granulaire a d'abord été déployée dans Next.js derrière un flag, où elle a été testée sur un certain nombre d'utilisateurs de la première heure. Beaucoup ont constaté une réduction significative de la quantité totale de code JavaScript utilisé sur l'ensemble de leur site :
| Site Web | Évolution totale du JS | Différence (en %) |
|---|---|---|
| https://www.barnebys.com/ | -238 Ko | -23 % |
| https://sumup.com/ | -220 ko | -30 % |
| https://www.hashicorp.com/ | -11 Mo | -71 % |
La version finale a été livrée par défaut dans la version 9.2.
Gatsby
Gatsby utilisait la même approche, à savoir une heuristique basée sur l'utilisation pour définir les modules communs :
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 leur configuration webpack pour adopter une stratégie de segmentation granulaire similaire, ils ont également constaté des réductions importantes de JavaScript sur de nombreux sites de grande taille :
| Site Web | Évolution totale du JS | 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 % |
Consultez la demande d'extraction pour comprendre comment ils ont implémenté cette logique dans leur configuration webpack, qui est fournie par défaut dans la version 2.20.7.
Conclusion
Le concept d'envoi de fragments précis n'est pas spécifique à Next.js, Gatsby ou même webpack. Tout le monde devrait envisager d'améliorer la stratégie de segmentation de son application si elle suit une approche de bundle "commun" volumineux, quel que soit le framework ou le bundler de modules utilisé.
- Si vous souhaitez voir les mêmes optimisations de segmentation appliquées à une application React standard, consultez cet exemple d'application React. Il utilise une version simplifiée de la stratégie de segmentation granulaire et peut vous aider à commencer à appliquer le même type de logique à votre site.
- Pour Rollup, les fragments sont créés de manière précise par défaut. Consultez
manualChunkssi vous souhaitez configurer manuellement le comportement.