Tree shaking des icônes
Importez les icônes que vous utilisez : le reste du jeu d'icônes n'entre pas dans votre bundle. Ce guide explique ce qui permet ce fonctionnement, ce qui le casse sans prévenir et à quoi ressemblent les chiffres dans un build réel.
Prérequis
Tout bundler capable d'appliquer le tree shaking aux modules ES : Vite, Rollup, webpack 5 ou Rspack en mode production, Parcel, ou esbuild avec le bundling et le code splitting activés. Il n'y a rien à configurer. Utilisez des imports nommés depuis les points d'entrée ESM :
import { HomeIcon, SearchIcon } from 'glyphfox/react'; import { iconHome } from 'glyphfox'; // icon data, also tree shaken import { renderToString } from 'glyphfox/core'; // the engine without icon data
Il en va de même pour glyphfox/vue, construit exactement comme le point d'entrée React, et pour les composants Svelte, qui comptent eux aussi un fichier par icône. Les serveurs de développement n'appliquent pas le tree shaking : jugez donc la taille uniquement sur des builds de production.
Fonctionnement
Trois propriétés du paquet permettent à un bundler d'écarter toutes les icônes que vous n'importez pas.
- Un module par icône. Le build génère un fichier distinct pour chaque icône, et l'index de chaque point d'entrée ne fait que les réexporter. Un bundler peut suivre la réexportation que vous utilisez et ignorer les autres.
- Appels purs. Chaque composant est créé avec
/*#__PURE__*/ createIcon(icon), ce qui indique au minifieur que l'appel n'a aucun effet qui mérite d'être conservé lorsque son résultat n'est pas utilisé. - Effets de bord déclarés. Le champ
sideEffectsdepackage.jsonliste les seuls fichiers qui agissent à l'import. Tout autre module peut être entièrement ignoré lorsqu'aucun de ses exports n'est utilisé.
import { createIcon } from '../runtime.js'; import icon from '../../icons/home.js'; export const HomeIcon = /*#__PURE__*/ createIcon(icon); export default HomeIcon;
"sideEffects": [ "./dist/web/index.js", "./dist/web/glyphfox.min.js", "./dist/runtime/auto.js", "*.css" ]
Les fichiers à effets de bord sont ceux qui sont censés agir à l'import : glyphfox/web définit l'élément personnalisé avec toutes les icônes, le fichier CDN met tout en place pour une page sans build, et glyphfox/runtime remplace les éléments data-glyph.
Tailles mesurées
Voici un exemple de mesure du build actuel, réalisée avec esbuild 0.28 : ESM minifié avec code splitting, en ne comptant que les chunks chargés dès le départ, React étant marqué comme externe. Prenez-la comme une indication des proportions, pas comme une promesse ; votre bundler et votre propre code feront varier les chiffres.
| Ce que l'application importe | Minifié | Gzip |
|---|---|---|
| Une icône React et le runtime commun | 76,0 Ko | 27,6 Ko |
| 10 icônes React | 79,3 Ko | 29,7 Ko |
| 50 icônes React | 90,9 Ko | 37,9 Ko |
import * as Icons avec une clé dynamique (toutes les icônes) | 214,7 Ko | non mesuré |
renderToString de glyphfox/core et une icône | 37,4 Ko | 15,2 Ko |
toSvg depuis le point d'entrée principal glyphfox | 153,8 Ko | 45,0 Ko |
Le fichier CDN glyphfox.min.js, sans étape de build | 216,4 Ko | 62,4 Ko |
sprite.svg avec toutes les icônes | 117,9 Ko | 15,7 Ko |
Deux points ressortent. Le runtime constitue la part fixe : le rendu, les trois axes, les modificateurs, la fusion, le morph et les préréglages d'animation sont partagés par toutes les icônes et coûtent autant pour une icône que pour cinquante. Chaque icône supplémentaire coûte peu, environ 0,2 Ko gzip en moyenne entre les builds à 10 et à 50 icônes. Un build de test avec Vite 8.3.5 va dans le même sens : le chunk d'entrée avec HomeIcon pesait 81,5 Ko minifié et 27,7 Ko gzip.
Icônes demandées par nom
Icon avec un nom, morphTo et Fusion acceptent des noms sous forme de chaînes. Lorsqu'un nom n'a été importé nulle part, le moteur le charge à la demande depuis le dossier des icônes :
let loader = (name) => import(`../icons/${name}.js`).then((m) => m.default);
Le sort de cet import dépend du bundler. Ni webpack (et donc Next.js) ni esbuild ne peuvent savoir quels noms seront demandés : ils préparent donc un chunk pour chaque fichier correspondant (un par icône, de quelques centaines d'octets chacun), qui n'est téléchargé que lorsqu'une page affiche un nom non importé. Vite ne transforme pas les imports dynamiques situés dans node_modules : son build de production n'émet aucun chunk d'icône et conserve l'import tel quel, si bien qu'un nom ni importé ni enregistré reste vide en production, alors qu'il fonctionne en développement, où la dépendance est pré-bundlée.
Si une application a réellement besoin de n'importe quel nom à l'exécution, comme dans un sélecteur d'icônes, réactivez la transformation pour glyphfox. Vite émet alors les chunks par icône et affiche un avertissement INEFFECTIVE_DYNAMIC_IMPORT pour chaque icône également importée directement, ce qui est sans conséquence :
export default { build: { dynamicImportVarsOptions: { exclude: [/node_modules\/(?!glyphfox\/)/] }, }, };
Si vous connaissez les noms à l'avance, enregistrez leurs données : plus rien n'est récupéré à l'exécution, quel que soit le bundler. Si vous servez vous-même les données des icônes, remplacez le chargeur ; celui par défaut reste dans le code, donc webpack émet toujours les chunks, mais l'application ne les demandera pas :
import { registerIcons, setIconLoader } from 'glyphfox/core'; import { iconSettings, iconUsers } from 'glyphfox'; registerIcons([iconSettings, iconUsers]); // optional: load other names from one JSON file you host (a copy of glyphfox/icons.json) let all; setIconLoader(async (name) => { all ??= await fetch('/icons.json').then((r) => r.json()); return all[name] ?? null; });
Livrer un sous-ensemble fixe
Les pages sans bundler, les paquets de design system et les applications natives ont souvent besoin d'une liste fixe d'icônes avec des props fixes. La CLI génère exactement cela, à partir de noms passés en ligne de commande ou d'un fichier contenant un nom par ligne :
# a sprite with only these icons npx glyphfox build home search file+check --type sprite --out public/icons # a CSS file with one mask class per icon npx glyphfox build home search bell --type css --out public/icons # a small package of SVG strings, with the props baked in npx glyphfox build --from icons.txt --type package --name @acme/icons --weight 1.5 --out acme-icons
Un sprite ou un fichier CSS de sous-ensemble ne contient aucun moteur, ce qui surpasse n'importe quel bundle lorsque les icônes ne changent jamais. Pour les pages qui chargent le fichier CDN, un sous-ensemble représente généralement la plus grosse économie à lui seul : par commodité, le fichier complet embarque toutes les icônes. Le guide des sprites SVG et le guide HTML et CDN couvrent ces deux configurations.
Vérifier votre propre bundle
Les données d'icône conservent leurs clés name et body après minification, ce qui permet un comptage rapide sans aucun analyseur. Dans un build Vite, les icônes que vous importez directement se trouvent dans le chunk d'entrée, à côté de l'icône de substitution nommée empty que le runtime affiche pendant le chargement d'un nom :
npx vite build grep -ohE 'name:["`][a-z0-9-]+["`],body' dist/assets/index-*.js | grep -v empty | sort -u | wc -l
Le nombre obtenu doit correspondre aux icônes que vous importez. S'il est proche de la taille du jeu complet, l'un des schémas décrits ci-dessous fait tout embarquer. D'autres bundlers peuvent déplacer les modules partagés dans des chunks séparés : avec eux, faites le même comptage sur chaque fichier que la page charge dès le départ, en laissant de côté les chunks d'icônes différés.
Pièges courants
import *avec une clé dynamiqueIcons[name]sur un import d'espace de noms oblige le bundler à conserver toutes les icônes. Affichez plutôt<Icon name={name} />, qui ne charge que les noms qu'il rencontre.requireen CommonJSrequire('glyphfox/react')se résout versindex.cjs, déjà bundlé : un seul fichier contenant toutes les icônes, qu'aucun bundler ne peut découper. Assurez-vous que le code de votre application utiliseimportafin que le build ESM soit retenu.toSvgcôté clienttoSvgprend des noms : il a donc besoin du registre complet et embarque toutes les icônes. Dans le code exécuté par le navigateur, utilisezrenderToStringdeglyphfox/coreavec des données d'icône importées. Côté serveur, la taille n'a pas d'importance.- esbuild sans splitting
- Sans
splitting: true, esbuild intègre directement chaque module que le chargeur dynamique peut atteindre : une icône a donné un fichier unique de 201 Ko minifié, au lieu de 76 Ko. Il en va de même pour toute sortie qui intègre les imports dynamiques, comme un build de bibliothèque IIFE ou UMD. glyphfox/webpour quelques icônes- Ce point d'entrée définit volontairement l'élément avec le jeu d'icônes complet. Pour un sous-ensemble, utilisez
defineGlyphFoxdeglyphfox/web/elementetregisterIcons. - Juger la taille en développement
- Les serveurs de développement chargent tous les modules d'un fichier barrel. Les imports profonds comme
glyphfox/react/icons/homeaccélèrent le développement, mais c'est le build de production qui compte pour la taille.
Questions fréquentes
Pourquoi la sortie de mon build contient-elle des centaines de petits fichiers ?
Avec webpack ou esbuild, ce sont les chunks différés du chargeur à la demande, un par icône, préparés parce qu'un nom pourrait être demandé sous forme de chaîne à l'exécution. Ils restent sur le serveur tant qu'aucune page n'affiche un nom non importé : ils ne coûtent donc rien aux utilisateurs. Si vous enregistrez vos noms avec registerIcons, ils ne sont jamais demandés. Un build de production Vite ne les crée pas, comme l'explique la section Icônes demandées par nom.
Combien coûte chaque icône supplémentaire ?
Dans la mesure ci-dessus, environ 0,2 Ko gzip par icône, en plus du runtime commun qui pèse près de 28 Ko gzip. Le runtime n'est payé qu'une fois : la deuxième icône coûte donc bien moins cher que la première.