Temas

Tree shaking de iconos

Importa los iconos que usas y el resto del conjunto se queda fuera de tu bundle. Esta guía muestra qué hace que esto funcione, qué lo rompe sin que te des cuenta y cómo quedan las cifras en un build real.

Qué necesitas

Cualquier bundler que aplique tree shaking a módulos ES: Vite, Rollup, webpack 5 o Rspack en modo producción, Parcel, o esbuild con el empaquetado y la división de código activados. No hay nada que configurar. Usa importaciones con nombre desde los puntos de entrada 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

Lo mismo se aplica a glyphfox/vue, que se compila exactamente igual que el punto de entrada de React, y a los componentes de Svelte, que también son un archivo por icono. Los servidores de desarrollo no aplican tree shaking, así que evalúa el tamaño solo en builds de producción.

Cómo funciona

Tres características del paquete permiten que un bundler descarte todos los iconos que no importas.

  1. Un módulo por icono. El build escribe cada icono en su propio archivo, y el índice de cada punto de entrada solo los reexporta. Un bundler puede seguir la única reexportación que usas e ignorar las demás.
  2. Llamadas puras. Cada componente se crea con /*#__PURE__*/ createIcon(icon), lo que indica al minificador que la llamada no tiene ningún efecto que merezca conservarse cuando su resultado no se usa.
  3. Efectos secundarios declarados. El campo sideEffects de package.json enumera los únicos archivos que hacen algo al importarse. Cualquier otro módulo se puede omitir por completo cuando no se usa ninguna de sus exportaciones.
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"
]

Los archivos con efectos secundarios son los que están pensados para hacer algo al importarse: glyphfox/web define el custom element con todos los iconos, el archivo de la CDN prepara todo lo necesario para una página sin build y glyphfox/runtime reemplaza los elementos data-glyph.

Tamaños medidos

Una medición de ejemplo del build actual, hecha con esbuild 0.28: ESM minificado con división de código, contando solo los chunks que se cargan al inicio y con React marcado como externo. Tómala como una referencia de proporciones, no como una promesa; tu bundler y tu propio código harán variar las cifras.

Qué importa la appMinificadoGzip
Un icono de React más el runtime compartido76.0 KB27.6 KB
10 iconos de React79.3 KB29.7 KB
50 iconos de React90.9 KB37.9 KB
import * as Icons con una clave dinámica (todos los iconos)214.7 KBno medido
renderToString de glyphfox/core más un icono37.4 KB15.2 KB
toSvg desde el punto de entrada principal glyphfox153.8 KB45.0 KB
El archivo de la CDN glyphfox.min.js, sin paso de compilación216.4 KB62.4 KB
sprite.svg con todos los iconos117.9 KB15.7 KB

Destacan dos cosas. El runtime es la parte fija: el renderizado, los tres ejes, los modificadores, la fusión, el morph y los presets de animación son comunes a todos los iconos y cuestan lo mismo para un icono que para cincuenta. Cada icono adicional sale barato: de media, unos 0.2 KB con gzip entre los builds de 10 y de 50 iconos. Un build de prueba con Vite 8.3.5 lo corroboró: el chunk de entrada con HomeIcon ocupó 81.5 KB minificado y 27.7 KB con gzip.

Iconos solicitados por nombre

Icon con un nombre, morphTo y Fusion aceptan nombres como cadenas. Cuando un nombre no se ha importado en ninguna parte, el motor lo carga bajo demanda desde la carpeta de iconos:

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

Lo que ocurre con esa importación depende del bundler. webpack (y, por tanto, Next.js) y esbuild no pueden saber qué nombres se van a pedir, así que preparan un chunk para cada archivo que coincide: uno por icono, de unos cientos de bytes cada uno, que solo se descarga cuando una página renderiza un nombre que no se importó. Vite no transforma las importaciones dinámicas dentro de node_modules: su build de producción no emite chunks de iconos y mantiene la importación tal como está escrita, así que un nombre que no se importó ni se registró queda vacío en producción, aunque funciona en desarrollo, donde la dependencia está preempaquetada.

Si una app realmente necesita cualquier nombre en tiempo de ejecución, como un selector de iconos, vuelve a activar la transformación para glyphfox. Vite emitirá entonces los chunks por icono y mostrará un aviso INEFFECTIVE_DYNAMIC_IMPORT por cada icono que también se importe directamente, algo inofensivo:

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

Cuando conoces los nombres de antemano, registra sus datos y no se descargará nada en tiempo de ejecución, con cualquier bundler. Si sirves tú mismo los datos de los iconos, sustituye el cargador; el predeterminado sigue en el código, así que webpack sigue emitiendo los chunks, pero la app no los solicitará:

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

Distribuye un subconjunto fijo

Las páginas sin bundler, los paquetes de sistemas de diseño y las apps nativas suelen buscar una lista fija de iconos con props fijas. La CLI genera exactamente eso, a partir de nombres en la línea de comandos o de un archivo con un nombre por línea:

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 o un archivo CSS de subconjunto no contiene ningún motor, lo que supera a cualquier bundle cuando los iconos no cambian nunca. En las páginas que cargan el archivo de la CDN, un subconjunto suele ser el mayor ahorro individual: el archivo completo incluye todos los iconos por comodidad. La guía de sprites SVG y la guía de HTML y CDN explican ambas configuraciones.

Comprueba tu propio bundle

Los datos de los iconos conservan sus claves name y body tras la minificación, lo que permite hacer un recuento rápido sin ningún analizador. En un build de Vite, los iconos que importas directamente están en el chunk de entrada, junto al marcador de posición llamado empty que el runtime dibuja mientras se carga un nombre:

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

El número debería coincidir con los iconos que importas. Si se acerca al tamaño del conjunto completo, alguno de los patrones descritos más abajo lo está arrastrando todo. Otros bundlers pueden mover los módulos compartidos a chunks separados, así que con ellos haz el mismo recuento en todos los archivos que la página carga al inicio y deja fuera los chunks de iconos de carga diferida.

Errores comunes

import * con una clave dinámica
Icons[name] sobre una importación de namespace obliga al bundler a conservar todos los iconos. En su lugar, renderiza <Icon name={name} />, que solo carga los nombres que encuentra.
require de CommonJS
require('glyphfox/react') se resuelve al index.cjs empaquetado, un único archivo con todos los iconos que ningún bundler puede dividir. Asegúrate de que el código de tu app usa import para que se elija el build ESM.
toSvg en el cliente
toSvg recibe nombres, así que necesita el registro completo y arrastra consigo todos los iconos. En el código del navegador, usa renderToString de glyphfox/core con datos de iconos importados. En el servidor, el tamaño no importa.
esbuild sin división de código
Sin splitting: true, esbuild incrusta todos los módulos a los que puede llegar el cargador dinámico: un solo icono ocupó 201 KB minificado en un único archivo, en lugar de 76 KB. Lo mismo ocurre con cualquier salida que incruste las importaciones dinámicas, como un build de biblioteca IIFE o UMD.
glyphfox/web para unos pocos iconos
Ese punto de entrada define a propósito el elemento con el conjunto completo. Para un subconjunto, usa defineGlyphFox de glyphfox/web/element y registerIcons.
Evaluar el tamaño en desarrollo
Los servidores de desarrollo cargan todos los módulos de un archivo barrel. Las importaciones profundas como glyphfox/react/icons/home agilizan el desarrollo, pero lo que cuenta para el tamaño es el build de producción.

Preguntas frecuentes

¿Por qué la salida de mi build contiene cientos de archivos diminutos?

Con webpack o esbuild, son los chunks diferidos del cargador bajo demanda, uno por icono, preparados porque un nombre podría solicitarse como cadena en tiempo de ejecución. Se quedan en el servidor salvo que una página renderice un nombre que no se importó, así que no les cuestan nada a los usuarios. Si registras tus nombres con registerIcons, nunca se solicitan. Un build de producción de Vite no los crea, como se explica en iconos solicitados por nombre.

¿Cuánto cuesta cada icono adicional?

En la medición anterior, unos 0.2 KB con gzip por icono, además del runtime compartido de unos 28 KB con gzip. El runtime se paga una sola vez, así que el segundo icono sale mucho más barato que el primero.

Iconos de esta guía