Tópicos

Tree shaking de ícones

Importe os ícones que você usa, e o resto do conjunto fica fora do seu bundle. Este guia mostra o que faz isso funcionar, o que quebra esse comportamento sem nenhum aviso e como ficam os números em um build real.

Do que você precisa

Qualquer bundler que faça tree shaking de módulos ES: Vite, Rollup, webpack 5 ou Rspack em modo de produção, Parcel, ou esbuild com bundling e code splitting ativados. Não há nada para configurar. Use imports nomeados a partir dos pontos 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

O mesmo vale para glyphfox/vue, que é gerado exatamente como o ponto de entrada do React, e para os componentes Svelte, que também são um arquivo por ícone. Servidores de desenvolvimento não fazem tree shaking, então avalie o tamanho só em builds de produção.

Como funciona

Três características do pacote permitem que o bundler descarte todos os ícones que você não importa.

  1. Um módulo por ícone. O build grava cada ícone em um arquivo próprio, e o index de cada ponto de entrada só faz o re-export desses arquivos. Um bundler consegue seguir só o re-export que você usa e ignorar os outros.
  2. Chamadas puras. Cada componente é criado com /*#__PURE__*/ createIcon(icon), o que indica ao minificador que a chamada não tem nenhum efeito que valha manter quando o resultado não é usado.
  3. Efeitos colaterais declarados. O campo sideEffects do package.json lista os únicos arquivos que fazem algo ao serem importados. Qualquer outro módulo pode ser ignorado por completo quando nenhum dos exports dele é usado.
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"
]

Os arquivos com efeitos colaterais são justamente os que existem para trabalhar na importação: glyphfox/web define o custom element com todos os ícones, o arquivo da CDN prepara tudo para uma página sem build e glyphfox/runtime substitui os elementos data-glyph.

Tamanhos medidos

Um exemplo de medição do build atual, feita com esbuild 0.28: ESM minificado com code splitting, contando só os chunks carregados logo de início, com o React marcado como externo. Use os números como referência de proporção, não como promessa; o seu bundler e o seu próprio código vão alterar esses valores.

O que o app importaMinificadoGzip
Um ícone React mais o runtime compartilhado76,0 KB27,6 KB
10 ícones React79,3 KB29,7 KB
50 ícones React90,9 KB37,9 KB
import * as Icons com chave dinâmica (todos os ícones)214,7 KBnão medido
renderToString de glyphfox/core mais um ícone37,4 KB15,2 KB
toSvg do ponto de entrada principal glyphfox153,8 KB45,0 KB
O arquivo da CDN glyphfox.min.js, sem etapa de build216,4 KB62,4 KB
sprite.svg com todos os ícones117,9 KB15,7 KB

Duas coisas chamam a atenção. O runtime é a parte fixa: renderização, os três eixos, modificadores, fusão, morph e os presets de animação são compartilhados por todos os ícones e custam o mesmo para um ícone ou para cinquenta. Cada ícone extra sai barato, cerca de 0,2 KB com gzip em média entre os builds de 10 e de 50 ícones. Um build de teste com Vite 8.3.5 confirmou: o chunk de entrada com HomeIcon ficou em 81,5 KB minificado e 27,7 KB com gzip.

Ícones pedidos pelo nome

Icon com um nome, morphTo e Fusion aceitam nomes como strings. Quando um nome não foi importado em lugar nenhum, o motor carrega o ícone sob demanda da pasta de ícones:

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

O que acontece com esse import depende do bundler. O webpack (e, portanto, o Next.js) e o esbuild não têm como saber quais nomes serão pedidos, então preparam um chunk para cada arquivo que corresponde ao padrão: um por ícone, com algumas centenas de bytes cada, baixado só quando uma página renderiza um nome que não foi importado. O Vite não transforma imports dinâmicos dentro de node_modules: o build de produção dele não emite chunks de ícones e mantém o import como foi escrito, então um nome que não foi importado nem registrado fica vazio em produção, embora funcione em desenvolvimento, onde a dependência é pré-empacotada.

Se um app realmente precisa de qualquer nome em tempo de execução, como em um seletor de ícones, inclua o glyphfox de volta na transformação. O Vite passa então a emitir os chunks por ícone e mostra um aviso INEFFECTIVE_DYNAMIC_IMPORT para cada ícone que também é importado diretamente, o que é inofensivo:

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

Quando você conhece os nomes de antemão, registre os dados deles e nada é buscado em tempo de execução, com qualquer bundler. Se você mesmo serve os dados dos ícones, substitua o loader; o loader padrão continua no código, então o webpack ainda emite os chunks, mas o app não vai requisitá-los:

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

Como entregar um subconjunto fixo

Páginas sem bundler, pacotes de design system e apps nativos muitas vezes querem uma lista fixa de ícones com props fixas. A CLI gera exatamente isso, a partir de nomes na linha de comando ou de um arquivo com um nome por linha:

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

Um sprite ou arquivo CSS com um subconjunto não contém motor nenhum, o que supera qualquer bundle quando os ícones nunca mudam. Para páginas que carregam o arquivo da CDN, um subconjunto costuma ser a maior economia individual: o arquivo completo traz todos os ícones por conveniência. O guia de sprites SVG e o guia de HTML e CDN cobrem as duas configurações.

Confira o seu bundle

Os dados de ícone mantêm as chaves name e body depois da minificação, o que permite uma contagem rápida sem nenhum analisador. Em um build do Vite, os ícones que você importa diretamente ficam no chunk de entrada, ao lado do placeholder chamado empty, que o runtime desenha enquanto um nome carrega:

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

O número deve bater com os ícones que você importa. Se estiver perto do tamanho do conjunto inteiro, um dos padrões abaixo está puxando tudo para dentro. Outros bundlers podem mover módulos compartilhados para chunks separados; com eles, faça a mesma contagem em todos os arquivos que a página carrega logo de início e deixe de fora os chunks de ícones carregados sob demanda.

Armadilhas comuns

import * com chave dinâmica
Icons[name] em um import de namespace obriga o bundler a manter todos os ícones. Em vez disso, renderize <Icon name={name} />, que carrega só os nomes que de fato aparecem.
require do CommonJS
require('glyphfox/react') resolve para o index.cjs empacotado, um único arquivo com todos os ícones que nenhum bundler consegue dividir. Garanta que o código do seu app use import para que o build ESM seja escolhido.
toSvg no cliente
toSvg recebe nomes, então precisa do registro completo e leva todos os ícones junto. No código do navegador, use renderToString de glyphfox/core com dados de ícone importados. No servidor, o tamanho não importa.
esbuild sem splitting
Sem splitting: true, o esbuild embute todos os módulos que o loader dinâmico consegue alcançar: um único ícone chegou a 201 KB minificado em um só arquivo, em vez de 76 KB. O mesmo acontece com qualquer saída que embute imports dinâmicos, como um build de biblioteca IIFE ou UMD.
glyphfox/web para poucos ícones
Esse ponto de entrada define o elemento com o conjunto inteiro de propósito. Para um subconjunto, use defineGlyphFox de glyphfox/web/element e registerIcons.
Avaliar o tamanho em desenvolvimento
Servidores de desenvolvimento carregam todos os módulos de um barrel file. Imports profundos como glyphfox/react/icons/home deixam o desenvolvimento mais rápido, mas o que conta para o tamanho é o build de produção.

Perguntas frequentes

Por que a saída do meu build tem centenas de arquivos minúsculos?

Com webpack ou esbuild, são os chunks lazy do loader sob demanda, um por ícone, preparados porque um nome pode ser pedido como string em tempo de execução. Eles ficam no servidor, a menos que uma página renderize um nome que não foi importado, então não custam nada para os usuários. Se você registrar os seus nomes com registerIcons, eles nunca serão requisitados. Um build de produção do Vite não cria esses arquivos, como explicado em ícones pedidos pelo nome.

Quanto custa cada ícone extra?

Na medição acima, cerca de 0,2 KB com gzip por ícone, além do runtime compartilhado de aproximadamente 28 KB com gzip. O runtime é pago uma única vez, então o segundo ícone sai muito mais barato do que o primeiro.

Ícones deste guia