Du côté des raisons, le banc des accusés peut varier en fonction de la taille de l’application. Cet article se concentrera notamment sur les importations de Lodash, les mises à jour des dépendances, l’importation adéquate de certaines méthodes et l’optimisation des importations de composants et des dépendances.
L’objectif final est donc de réduire la taille du bundle et d’avoir une application aussi petite que possible pour faire plaisir à la fois aux utilisateurs et aux robots.
1. Faire un état des lieux
Avant de procéder à toute optimisation, il est préférable de commencer par un état des lieux. Cela vous aidera non seulement à mesurer vos efforts, mais aussi à justifier le temps passé sur une tâche qui pourrait sembler inutile à un œil non averti.
Pour l’exemple, j’ai choisi un projet interne de Coteries qui n’avait pas encore été optimisé. Le projet est construit en utilisant NextJS 12, Chakra-UI, Fontawesome et contient de nombreuses autres dépendances telles que Lodash, DayJS, Mixpanel, Google Analytics…
Pour analyser et visualiser mon bundle, j’ai utilisé les librairies et paquets suivants:
- next-compose-plugin, qui aide à gérer les plugins sur le fichier de configuration de Next
- next-bundle-analyzer, qui permet de visualiser le bundle afin de voir ce qui prend de la place.
- source-map-explorer, qui aide à visualiser plus précisément le contenu du bundle. Ce paquet peut être installé globalement.
Vous trouverez ci-dessous le fichier de configuration de NextJS. Les source maps doivent être activées afin de pouvoir effectuer les différentes analyses. Pensez à les désactiver une fois les investigations terminées!
const withPlugins = require("next-compose-plugins");
const { withPlausibleProxy } = require("next-plausible");
const withBundleAnalyzer = require("@next/bundle-analyzer");
const nextTranslate = require("next-translate");
const plausiblePlugin = withPlausibleProxy;
const bundleAnalyzer = withBundleAnalyzer({
enabled: process.env.ANALYZE === "true",
});
const nextConfig = {
productionBrowserSourceMaps: process.env.ANALYZE === "true",
reactStrictMode: true,
images: {
formats: ["image/avif", "image/webp"],
domains: ["***"],
},
};
module.exports = withPlugins(
[plausiblePlugin, bundleAnalyzer, nextTranslate],
nextConfig
);
Pour utiliser la librairie next-bundle-analyzer, nous devons ajouter la commande suivante dans le package.json: "analyze": "ANALYZE=true next build".
Il est maintenant possible d’exécuter les commandes suivantes pour avoir une référence initiale:
pnpm run build
: construit le projet et donne des informations sur le premier chargement JS.pnpm run analyze
: donne une idée générale de la répartition du bundle.- source-map-explorer .next/static/**/*.js: donne des informations plus détaillées sur le paquet.
Voici les chiffres dans mon cas:
- 2.23 MB pour la taille du bundle avec analyze
- 2.08 MB pour la taille du bundle avec source-map-explorer
- 656 kB chargés avec 1.9 MB de ressources sur l’inspecteur réseau
En plus de ces chiffres, j’ai exécuté les tests web.dev/measure 5 fois pour obtenir un score de performance moyen. Ce chiffre nous aidera à voir si nous apportons de réelles améliorations ou si nos efforts sont vains. Le score moyen initial sur web.dev est de 69.2
2. Améliorer les imports Lodash
Lodash est une librairie assez standard, et les chances qu’elle soit utilisée dans votre projet sont plutôt élevées. Avec ses 46 millions de téléchargements par semaine, la librairie est très prisée, et l’utiliser correctement devient primordial.
L’image suivante montre comment l’optimisation des importations peut avoir un impact significatif sur la taille des fichiers. On observe un facteur de 10x selon la façon d’importer des méthodes!
import _ from "lodash"; // 73,13kB (gzip: 25,43kB)
import { isEmpty } from "lodash"; // 73,13kB (gzip: 25,43kB)
import isEmpty from "lodash/isEmpty"; // 7,04kB (gzip: 2,26kB)
Évidemment, c’est la dernière méthode qui est recommandée
Nous avons utilisé la deuxième méthode dans ce projet. La modification des importations est assez rapide, et il n’a fallu que 5 minutes pour effectuer le changement.
- 2.17 MB pour la taille du bundle avec analyze (☺️ -2.691%)
- 2.02 MB pour la taille du bundle avec source-map-explorer (☺️ -2.885%)
- toujours 69.2 comme score de performance moyen
- 632 kB chargés avec 1.8 MB de ressources sur l’inspecteur réseau (☺️ -3.659%)
Les changements sont assez faibles, mais l’effort pour les réaliser l’est tout autant. C’est pourquoi cela vaut la peine de prendre le temps d’apporter cette amélioration.
3. Utiliser des importations dynamiques
Next supporte les importations dynamiques ES2020. Cela signifie qu’il est possible d’importer dynamiquement les composants qui ne sont pas affichés par défaut.
Une modale, un avertissement d’erreur et une fonction déclenchée uniquement par l’interaction de l’utilisateur sont tous des éléments qui ne sont pas requis par défaut. Les importations dynamiques offrent un moyen simple de gérer ces cas.
La règle est plutôt simple. Tout ce qui est affiché de manière conditionnelle peut être importé dynamiquement. Recherchez tous les {VARIABLE && ... dans votre code, et vous devriez voir les composants qui peuvent être modifiés.
const ServiceBadge = dynamic(() => import(« ../Molecules/ServiceBadge »));
const ServiceHeader = ({ service, title }: Props) => {
const router = useRouter();
const isMobile = useIsMobile();
const keys = textStore((state) => state.keys);
return (
<HStack>
<BackButton onClick={() => router.replace(ROUTE_ROOT)} />
{!isMobile && <ServiceBadge icon={service.icon} color={service.color} />}
<Heading>{title}</Heading>
</HStack>
);
};
Dans ce cas, le ServiceBadge ne sera chargé que si l’utilisateur consulte le site depuis son mobile
Regardez également les méthodes qui sont exécutées sur les inputs de l’utilisateur. Importez ce dont elles ont besoin à l’intérieur des méthodes plutôt qu’en haut du fichier. De cette façon, vous ne chargerez les méthodes que lorsque vous en aurez besoin au lieu de les charger à chaque fois.
const handleLogout = async () => {
await subabase.auth.signOut();
setUser(null);
const showToast = await import("../../utils/showToast").then(
(mod) => mod.showToast
);
showToast(keys.logout_success, "success");
};
Nous ne chargeons le toast uniquement quand l’utilisateur clique sur le bouton de déconnexion
- 2.21 MB pour la taille du bundle avec analyze (+1.843%)
- 2.06 MB pour la taille du bundle avec source-map-explorer (+1.980%)
- 72 as the average performance score (+2.2)
- 568 kB chargés avec 1.6 MB de ressources sur l’inspecteur réseau (🤯 -10.127%)
L’importation dynamique des composants a un impact énorme sur la taille du JS chargé lors de l’ouverture du site web. Cette simple modification a permis de réduire la charge initiale de 10%, ce qui est plutôt significatif.
Cette réduction explique à elle seule pourquoi le score de performance s’est amélioré de plus de 2 points. L’utilisation d’importations dynamiques aide définitivement, et le résultat sera encore plus significatif avec une page plus complexe.
4. Analyser le Bundle
Cette étape est celle qui peut avoir l’impact le plus important, et c’est aussi celle qui peut prendre le plus de temps car elle dépend de nombreux facteurs. Grâce au bundle-analyzer et au source-map-explorer, nous avons une vue claire de ce qui se passe dans le bundle et de ce qui pourrait être modifié.
Il y a deux points à examiner à ce stade, à savoir s’il y a un gros fichier importé et s’il y a beaucoup de petits fichiers. Je ne sais pas combien un bundle optimisé devrait peser ni combien de fichiers il devrait contenir. Il est certain que ces deux chiffres devraient être le plus bas possible.
Au cours de mon enquête, j’ai découvert que certaines librairies étaient coûteuses et que react-syntax-highlight avait beaucoup de fichiers qui prenaient beaucoup d’espace. Vous pouvez les voir dans les captures d’écran suivantes.
La librairie Validator prend définitivement trop de place pour son utilisation et react-syntax-highlight semble suspecte!
L’analyse m’a fait comprendre que je devais examiner certaines librairies : validator, framer, mixpanel, fontawesome et react-code-blocs.
Je ne rentrerais pas trop dans les détails, mais voici un résumé rapide de ce que j’ai fait pour chaque librairie:
- validator: j’ai changé la façon dont j’importe les méthodes et j’utilise lodash isEmpty au lieu de celui du validator.
- framer : j’ai fait un peu d’animations en javascript vanilla pour les éléments plus spécifiques et j’ai gardé le tout tel quel pour les éléments plus complexes.
- mixpanel : je l’ai supprimé de la dépendance puisque les données n’étaient pas utilisées, et nous avions d’autres outils pour cela
- fontawesome : malheureusement, je n’ai pas été en mesure de réduire la taille des importations
- react-code-blocs : je l’ai supprimé de la librairie et utilisation directe de react-syntax-highlight à la place puisqu’il était possible de n’importer que ce qui était nécessaire.
En plus de ces actions, j’ai fait un peu de nettoyage des librairies et les ai remplacées par du TypeScript vanilla car elles n’apportaient pas beaucoup d’améliorations. J’ai également pris le temps de mettre à jour chaque paquet à sa dernière version dans l’espoir de gains significatifs (grâce à ncu).
Enfin, j’ai effectué une analyse du code et j’ai supprimé le code ancien et inutilisé. Cette étape ne permet pas forcément d’obtenir un plus petit bundle, mais elle est définitivement positive pour la qualité du code et la facilité d’utilisation.
- 1,59 MB pour la taille du bundle avec analyze (🔥🔥 -28,054%)
- 1,45 MB pour la taille du bundle avec source-map-explorer (🔥🔥 -29,612%)
- 73 comme score de performance moyen (+1)
- 489 kB chargés avec 1,3MB de ressources sur l’inspecteur réseau (🔥🔥 -13,908%)
Je me doutais bien que cette étape serait celle qui apporterait le plus de résultats, mais je ne m’attendais pas à de telles améliorations!
Enlever 30% de la taille du bundle en faisant simplement des choix plus intelligents en matière de sélection de librairies semble fou.
5. Ne pas utiliser les données du serveur dans useEffects
Next offre la possibilité de récupérer les données depuis le serveur et de les transmettre au client. Cela permet d’accélérer le chargement de la page puisque la bande passante du serveur est probablement plus rapide que votre connexion Internet. De plus, disposer des données lors du chargement de la page permet de réduire le nombre d’appels réseau.
Next offrons également la possibilité d’effectuer une génération statique par incrément. C’est une fonction que je recommande vivement, car elle permet un chargement quasi instantané de la page tout en garantissant la fraîcheur des données.
Obtenir des données du serveur est une excellente chose. Il serait en revanche dommage d’utiliser ces données et de les manipuler directement côté client.
Cela signifie que Next ne sera pas en mesure de compiler la page sur le serveur. Certains composants seront chargés côté client, entraînant un décalage de la mise en page (layout shift) ou de moins bonnes performances.
Prenons l’exemple suivant: nous récupérons des articles depuis le serveur puis régénérons la page par incréments toutes les 15 minutes (grâce à revalidate: 900).
Nous avons cependant la méthode useEffect dont l’exécution est garantie sur le client et qui définit un état où l’article est sauvegardé. C’est plutôt mauvais car Next ne pourra pas construire et pré-générer cette page.
export const getstaticProps: GetStaticProps = async () => {
const exampleArticles = await fetchExamplePosts();
return {
props: {
exampleArticles,
},
revalidate: 900,
};
};
const DisplayArticles = (exampleArticles: Props) => {
const [examples, setExample] = useState<ExamplePost[]>();
useEffect(() => {
setExample(exampleArticles);
}, []);
return (
<div>
{examples &&
examples.map((post) => (
<>
<img src={post.image.url} alt= » » />
<p>{post.nameEn}</p>
</>
))}
</div>
);
};
À la place, voici ce qu’il faudrait faire: accéder directement aux données dans le retour de la fonction, et l’article sera alors présent lorsque la page sera construite sur le serveur.
export const getstaticProps: GetStaticProps = async () => {
const exampleArticle = await fetchExamplePosts();
return {
props: {
exampleArticle,
},
revalidate: 900,
};
};
const DisplayArticles = (exampleArticles: Props) => {
return (
<div>
{!isEmpty(exampleArticles) &&
exampleArticles.map((post) => (
<>
<img src={post.image.url} alt= » » />
<p>{post.nameEn}</p>
</>
))}
</div>
);
};
Un dernier point: notez qu’il est normal que certaines données soient manipulées côté client. Il existe cependant des cas où les données sont récupérées à partir d’une API et affichées (comme c’est le cas dans les exemples ci-dessus).
Dans ces cas, veillez à ne pas manipuler les données côté client et affichez-les directement si elles sont déjà présentes lors du chargement des pages.
Pour conclure
Le tableau suivant montre l’impact de nos différentes actions sur la taille du bundle ou sur le score de performance. Chaque étape a un impact différent et les résultats que vous pourrez obtenir varieront considérablement.
Deux actions se démarquent majoritairement: l’utilisation d’importations dynamiques pour les éléments de l’interface utilisateur qui ne sont pas affichés par défaut, et la recherche des librairies qui alourdissent le bundle.
Les importations dynamiques sont une excellente fonctionnalité de NextJS, et elles permettent de télécharger moins de JavaScript. Il a été possible de réduire de 64 kB le JavaScript téléchargé lors du chargement de la page d’accueil, et la différence peut être encore plus significative pour des pages plus complexes entraînant un chargement quasi instantané de la page.
L’analyse du bundle est de loin la tâche qui m’aura pris le plus de temps. Remplacer ou corriger certaines librairies peut avoir des impacts importants et demander le refactoring de grandes parties du code, ce qui rend les choses plus difficiles. Restez rationnel et ne passez pas d’innombrables heures à essayer de supprimer une librairie essentielle à votre projet.
Je dois admettre qu’améliorer les importations de Lodash aura eu un impact significatif. En réalité, l’impact sur la taille dépendra surtout de l’importance de l’utilisation de la librairie dans votre projet. De plus, c’est un travail rapide, et il serait dommage de ne pas conserver les anciennes importations!
Résultat final de la compilation. Le résultat n'est peut-être pas très impressionnant mais les utilisateurs semblent l’apprécier!
En tous cas, les recherches que j’ai réalisées auront été passionnantes et m’auront permis de comprendre de nombreuses choses concernant Next et le bundling.
Ce fut une expérience d’apprentissage fantastique qui profitera à la fois à notre équipe en développement et à nos clients. Je peux déjà entrevoir ce que l’on pourra améliorer pour fournir des sites web encore plus rapides !
J’espère que vous serez en mesure de réduire le temps de chargement de votre site internet.
Tout le code des captures d’écran se trouve dans ce gist.