Thèmes

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 :

App.jsx
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.

  1. 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.
  2. 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é.
  3. Effets de bord déclarés. Le champ sideEffects de package.json liste les seuls fichiers qui agissent à l'import. Tout autre module peut être entièrement ignoré lorsqu'aucun de ses exports n'est utilisé.
glyphfox/dist/react/icons/home.js
import { createIcon } from '../runtime.js';
import icon from '../../icons/home.js';

export const HomeIcon = /*#__PURE__*/ createIcon(icon);
export default HomeIcon;
glyphfox/package.json
"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 importeMinifiéGzip
Une icône React et le runtime commun76,0 Ko27,6 Ko
10 icônes React79,3 Ko29,7 Ko
50 icônes React90,9 Ko37,9 Ko
import * as Icons avec une clé dynamique (toutes les icônes)214,7 Konon mesuré
renderToString de glyphfox/core et une icône37,4 Ko15,2 Ko
toSvg depuis le point d'entrée principal glyphfox153,8 Ko45,0 Ko
Le fichier CDN glyphfox.min.js, sans étape de build216,4 Ko62,4 Ko
sprite.svg avec toutes les icônes117,9 Ko15,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 :

glyphfox/dist/core/lookup.js
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 :

vite.config.js
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 :

icons.js
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 :

terminal
# 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 :

terminal
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é dynamique
Icons[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.
require en CommonJS
require('glyphfox/react') se résout vers index.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 utilise import afin que le build ESM soit retenu.
toSvg côté client
toSvg prend des noms : il a donc besoin du registre complet et embarque toutes les icônes. Dans le code exécuté par le navigateur, utilisez renderToString de glyphfox/core avec 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/web pour 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 defineGlyphFox de glyphfox/web/element et registerIcons.
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/home accé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.

Icônes de ce guide