Themen

Tree-Shaking für Icons

Importieren Sie die Icons, die Sie verwenden, und der Rest des Sets landet nicht in Ihrem Bundle. Dieser Leitfaden zeigt, was das möglich macht, was es unbemerkt aushebelt und wie die Zahlen in einem echten Build aussehen.

Voraussetzungen

Jeder Bundler, der Tree-Shaking für ES-Module beherrscht: Vite, Rollup, webpack 5 oder Rspack im Produktionsmodus, Parcel oder esbuild mit aktiviertem Bundling und Code-Splitting. Es gibt nichts zu konfigurieren. Verwenden Sie benannte Importe aus den ESM-Einstiegspunkten:

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

Dasselbe gilt für glyphfox/vue, das genau wie der React-Einstiegspunkt gebaut ist, und für die Svelte-Komponenten, die ebenfalls aus einer Datei pro Icon bestehen. Da Entwicklungsserver kein Tree-Shaking durchführen, sollten Sie die Größe nur anhand von Produktionsbuilds beurteilen.

So funktioniert es

Drei Eigenschaften des Pakets erlauben es einem Bundler, jedes Icon wegzulassen, das Sie nicht importieren.

  1. Ein Modul pro Icon. Der Build schreibt jedes Icon in eine eigene Datei, und der Index jedes Einstiegspunkts exportiert sie lediglich weiter. Ein Bundler kann dem einen Re-Export folgen, den Sie verwenden, und alle anderen ignorieren.
  2. Als pure markierte Aufrufe. Jede Komponente wird mit /*#__PURE__*/ createIcon(icon) erzeugt. Das teilt dem Minifier mit, dass der Aufruf keine erhaltenswerte Wirkung hat, wenn sein Ergebnis nicht verwendet wird.
  3. Deklarierte Seiteneffekte. Das Feld sideEffects in package.json listet die einzigen Dateien auf, die beim Import etwas tun. Jedes andere Modul darf komplett übersprungen werden, wenn keiner seiner Exporte verwendet wird.
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"
]

Die Dateien mit Seiteneffekten sind genau die, die beim Import etwas erledigen sollen: glyphfox/web definiert das Custom Element mit allen Icons, die CDN-Datei richtet alles für eine Seite ohne Build ein, und glyphfox/runtime ersetzt data-glyph-Elemente.

Gemessene Größen

Eine Beispielmessung des aktuellen Builds mit esbuild 0.28: minifiziertes ESM mit Code-Splitting, wobei nur die vorab geladenen Chunks zählen und React als extern markiert ist. Verstehen Sie die Werte als Orientierung für Größenverhältnisse, nicht als Versprechen; Ihr Bundler und Ihr eigener Code werden die Zahlen verschieben.

Was die App importiertMinifiziertGzip
Ein React-Icon plus die gemeinsame Runtime76,0 KB27,6 KB
10 React-Icons79,3 KB29,7 KB
50 React-Icons90,9 KB37,9 KB
import * as Icons mit dynamischem Schlüssel (alle Icons)214,7 KBnicht gemessen
renderToString aus glyphfox/core plus ein Icon37,4 KB15,2 KB
toSvg aus dem Haupteinstiegspunkt glyphfox153,8 KB45,0 KB
Die CDN-Datei glyphfox.min.js, ohne Build-Schritt216,4 KB62,4 KB
sprite.svg mit allen Icons117,9 KB15,7 KB

Zwei Dinge fallen auf. Die Runtime ist der feste Anteil: Rendering, die drei Achsen, Modifier, Fusion, Morph und die Animations-Presets werden von allen Icons gemeinsam genutzt und kosten bei einem Icon genauso viel wie bei fünfzig. Jedes weitere Icon ist günstig, im Schnitt etwa 0,2 KB gzip zwischen den Builds mit 10 und 50 Icons. Ein Test-Build mit Vite 8.3.5 bestätigte das: Der Entry-Chunk mit HomeIcon kam auf 81,5 KB minifiziert und 27,7 KB gzip.

Per Name angeforderte Icons

Icon mit einem Namen, morphTo und Fusion akzeptieren Namen als Strings. Wurde ein Name nirgends importiert, lädt die Engine ihn bei Bedarf aus dem Icon-Ordner:

glyphfox/dist/core/lookup.js
let loader = (name) => import(`../icons/${name}.js`).then((m) => m.default);

Was mit diesem Import passiert, hängt vom Bundler ab. webpack (und damit Next.js) und esbuild können nicht wissen, welche Namen angefragt werden, also bereiten sie für jede passende Datei einen Chunk vor: einen pro Icon, jeweils einige hundert Byte groß und nur dann heruntergeladen, wenn eine Seite einen Namen rendert, der nicht importiert wurde. Vite transformiert keine dynamischen Importe innerhalb von node_modules: Sein Produktionsbuild erzeugt keine Icon-Chunks und behält den Import unverändert bei. Ein Name, der weder importiert noch registriert wurde, bleibt in der Produktion daher leer, während er in der Entwicklung funktioniert, wo die Abhängigkeit vorab gebündelt wird.

Wenn eine App zur Laufzeit wirklich jeden beliebigen Namen braucht, etwa für eine Icon-Auswahl, nehmen Sie glyphfox wieder in die Transformation auf. Vite erzeugt dann die Chunks pro Icon und gibt für jedes Icon, das zusätzlich direkt importiert wird, eine Warnung INEFFECTIVE_DYNAMIC_IMPORT aus, die harmlos ist:

vite.config.js
export default {
  build: {
    dynamicImportVarsOptions: { exclude: [/node_modules\/(?!glyphfox\/)/] },
  },
};

Wenn Sie die Namen vorab kennen, registrieren Sie ihre Daten. Dann wird zur Laufzeit nichts nachgeladen, unabhängig vom Bundler. Wenn Sie Icon-Daten selbst ausliefern, ersetzen Sie den Loader; der Standard-Loader bleibt im Code, daher erzeugt webpack die Chunks weiterhin, aber die App fordert sie nicht an:

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;
});

Eine feste Teilmenge ausliefern

Seiten ohne Bundler, Pakete für Designsysteme und native Apps brauchen oft eine feste Liste von Icons mit festen Props. Genau das erstellt die CLI, aus Namen auf der Kommandozeile oder aus einer Datei mit einem Namen pro Zeile:

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

Ein Sprite oder eine CSS-Datei mit einer Teilmenge enthält überhaupt keine Engine und schlägt damit jedes Bundle, wenn sich die Icons nie ändern. Bei Seiten, die die CDN-Datei laden, ist eine Teilmenge meist die größte einzelne Einsparung: Die vollständige Datei enthält der Bequemlichkeit halber alle Icons. Der Leitfaden zu SVG-Sprites und der Leitfaden zu HTML und CDN behandeln beide Varianten.

Das eigene Bundle prüfen

Icon-Daten behalten ihre Schlüssel name und body auch nach der Minifizierung, deshalb ist eine schnelle Zählung ganz ohne Analyse-Tool möglich. In einem Vite-Build liegen die direkt importierten Icons im Entry-Chunk, neben dem Platzhalter namens empty, den die Runtime zeichnet, während ein Name geladen wird:

terminal
npx vite build
grep -ohE 'name:["`][a-z0-9-]+["`],body' dist/assets/index-*.js | grep -v empty | sort -u | wc -l

Die Zahl sollte der Anzahl der importierten Icons entsprechen. Liegt sie nahe an der Größe des gesamten Sets, zieht eines der unten beschriebenen Muster alles mit hinein. Andere Bundler verschieben gemeinsam genutzte Module unter Umständen in separate Chunks. Führen Sie die Zählung dort über alle Dateien aus, die die Seite vorab lädt, und lassen Sie die nachgeladenen Icon-Chunks weg.

Stolperfallen

import * mit dynamischem Schlüssel
Icons[name] auf einem Namespace-Import zwingt den Bundler, alle Icons zu behalten. Rendern Sie stattdessen <Icon name={name} />, das nur die Namen lädt, auf die es tatsächlich trifft.
require aus CommonJS
require('glyphfox/react') wird zur gebündelten Datei index.cjs aufgelöst, die alle Icons enthält und von keinem Bundler aufgeteilt werden kann. Achten Sie darauf, dass Ihr App-Code import verwendet, damit der ESM-Build gewählt wird.
toSvg im Client
toSvg nimmt Namen entgegen, braucht daher die vollständige Registry und bringt alle Icons mit. Verwenden Sie im Browsercode renderToString aus glyphfox/core mit importierten Icon-Daten. Auf dem Server spielt die Größe keine Rolle.
esbuild ohne Splitting
Ohne splitting: true bettet esbuild jedes Modul ein, das der dynamische Loader erreichen kann: Ein Icon ergab so 201 KB minifiziert in einer einzigen Datei statt 76 KB. Dasselbe passiert bei jeder Ausgabe, die dynamische Importe inline einbettet, etwa bei einem Library-Build als IIFE oder UMD.
glyphfox/web für wenige Icons
Dieser Einstiegspunkt definiert das Element absichtlich mit dem gesamten Set. Für eine Teilmenge verwenden Sie defineGlyphFox aus glyphfox/web/element und registerIcons.
Größe in der Entwicklung beurteilen
Entwicklungsserver laden jedes Modul einer Barrel-Datei. Deep Imports wie glyphfox/react/icons/home beschleunigen die Entwicklung, für die Größe zählt aber der Produktionsbuild.

Häufige Fragen

Warum enthält meine Build-Ausgabe Hunderte winziger Dateien?

Bei webpack oder esbuild sind das die Lazy-Chunks des Loaders, der Icons bei Bedarf nachlädt, einer pro Icon. Sie werden vorbereitet, weil ein Name zur Laufzeit als String angefordert werden könnte. Sie bleiben auf dem Server, solange keine Seite einen nicht importierten Namen rendert, und kosten Nutzer daher nichts. Wenn Sie Ihre Namen mit registerIcons registrieren, werden sie nie angefordert. Ein Vite-Produktionsbuild erzeugt sie gar nicht, wie unter Per Name angeforderte Icons erklärt.

Wie viel kostet jedes weitere Icon?

In der Messung oben etwa 0,2 KB gzip pro Icon, zusätzlich zur gemeinsamen Runtime von rund 28 KB gzip. Die Runtime bezahlen Sie nur einmal, daher ist das zweite Icon viel günstiger als das erste.

Icons in diesem Leitfaden