12 KiB
Bundle audit & optimization — activeUIX
Medición realizada el 2026-06-05 sobre el build de producción real (
npm run build,adapter-static, 145 rutas). Branchactive-uix. Todas las cifras "medidas" salen debuild/_app/immutable. Las cifras de otros frameworks son públicas de referencia (no medidas en este repo) y están marcadas como tales.
TL;DR
El JS está sano y bien tree-shakeado. El CSS es donde nos estamos excediendo — y es un problema estructural (un agregado monolítico), no de "demasiado código".
| Eje | Estado | Cifra clave |
|---|---|---|
| JS por página | ✅ Sano | entry 10.4 KB gz · runtime ~35 KB gz · componente mediana 7.3 KB gz |
| CSS por página | ⚠️ Exceso | 113.5 KB gz / 851 KB raw en toda página /uix |
| Code-splitting JS | ✅ Correcto | color engine, sema y componentes se cargan bajo demanda |
| Code-splitting CSS | ⏳ En progreso | Fase 0 hecha (−4.5 KB gz/página, doble-carga eliminada); 94 recipes aún en el agregado |
Evaluación, correcciones y plan (2026-06-06)
Verificado contra el build real (
build/_app/immutable) y el código. Las cifras del doc son exactas — 104@import, 10 auto-import, 851 KB raw / 113.5 KB gz monolito, base.css 340 KB, words.css 56 KB, foundation 52 KB gz: todo confirmado. Dos matices cambian la lectura.
Corrección 1 — los 10 "code-splittable" estaban DOBLE-CARGADOS
Los 10 que auto-importan su .css seguían también en index.css ⇒ su CSS
viajaba dos veces (probado: badge en su chunk badge.*.css y dentro del
monolito de 851 KB). Hicieron el paso 1 (auto-import) pero no el paso 2 (quitar
de index.css). No eran el ejemplo bueno — eran media migración que sumaba
bytes. → Resuelto en la Fase 0 (abajo).
Corrección 2 — el suelo real es base.css, y lleva contratos de token por componente
base.css (340 KB raw / 52 KB gz) es el 41 % del monolito y la pieza
única mayor. No es foundation pura: contiene los contratos de token por
componente ([data-badge], [data-card], --tag-group, --file-upload…).
Probado: tras quitar badge.css del agregado, [data-badge] sigue 8× en el
monolito — son de base.css (8 en base = 8 en monolito). Por tanto, aun con
el split perfecto toda página paga ~52 KB gz, y ese suelo solo baja podando
base.css (Fase 5), no partiendo recipes. El doc lo trataba como inamovible; es
la siguiente frontera.
Plan de ejecución
Mecánica por componente = 2 pasos: (1) import './x.css' en su .svelte;
(2) quitar su @import de index.css. El riesgo real no es eso — es el orden
de cascada (hoy index.css ordena deterministamente; menu-indicator.css
debe cargar después de los menús). Al code-splittear, el orden lo dicta qué
monta la página. Ese eje decide la Fase 1 (@layer o no).
| Fase | Qué | Riesgo | Verifica |
|---|---|---|---|
| 0 ✅ | Des-duplicar los 10 (quitar de index.css; ya auto-importan) |
nulo | hecho — ver resultado |
| 1 | Piloto dialog (compuesto + portalizado): dónde va el import · tokens cross-portal · decidir @layer |
medio | build + navegador |
| 2 | Partials compartidos (menu-indicator) + primitivas layout se quedan en foundation; auditar colisiones a igual especificidad |
medio | eidos-lint |
| 3 | Propagar a los ~94 restantes por lotes por familia (overlays·pickers·menús·forms·data·tipografía); nunca en cascada | medio | build+lint+visual/lote |
| 4 | index.css = foundation-only; medir CSS/página |
bajo | build |
| 5 | (track aparte) podar base.css a roles tematizados + calendarios no-gregorianos dynamic import |
alto | build |
Cross-coupling a vigilar (recipes agregados que referencian un componente
splitteado): auditado para los 10 — único caso words.css → [data-textarea], y
es seguro porque words-block-panel.svelte monta el TextArea de eidos (su
chunk carga el recipe). Repetir esta auditoría en cada lote de la Fase 3.
Fase 0 — resultado medido
index.css: 104 → 94@import. Monolito 850 879 → 814 420 raw (−36 KB) · 113 489 → 108 991 gz (−4.5 KB) — en toda página/uix.- Doble-carga eliminada: los 10 recipes ya solo viajan como chunk. image /
spinner / skeleton / password-field salen a 0 en el monolito; badge / card
dejan solo su contrato de token de
base.css; textarea deja solo la referencia dewords.css. Build verde, sin acoplamiento roto.
Metodología
npm run build→adapter-static, salida enbuild/.- Medición del cliente (
build/_app/immutable), que es lo que viaja al navegador — no el build SSR. - gzip calculado con
gzip -c(proxy de lo que sirve un CDN; brotli sería ~15-20 % menor todavía). - "Por página" = entry + runtime compartido + chunk único del nodo de ruta +
su CSS. El total del
build/(11 MB) es el catálogo completo de 145 demos, NO lo que sirve una app real — no usar esa cifra como referencia.
JS — sin exceso
| Qué | gzip | Cuándo carga |
|---|---|---|
entry (app + start) |
10.4 KB | toda página |
| Runtime Svelte/Kit (chunk compartido) | ~35 KB | toda página |
| Chunk único por componente (mediana) | 7.3 KB | solo su página |
| Chunk único más pesado (demo command/words) | 72 KB | solo esa demo |
Color engine (build-scheme / OKLCH / APCA) |
27.8 KB | solo páginas de tema (dynamic import) |
| Sema + a11y | 25.7 KB | bajo demanda |
Sistemas de calendario ($libs/days) |
~67 KB (2 chunks) | páginas con fecha/calendario |
Por qué está bien:
- El barrel de soma (
src/uix/soma/index.ts) expone soloSomay obliga a importar componentes por subpath explícito ($soma/components) → tree-shaking real, sin superficie duplicada. - El color engine se carga por dynamic import solo en las páginas de tema —
confirmado:
deriveScheme/generateScale/oklchNO están en el chunk de entrada ni en el runtime compartido. - Componentes code-split: una app que use 5 componentes no arrastra los 110.
Conclusión: no tocar el JS de soma/sema/color. Ya está bien.
CSS — el exceso real
Un único stylesheet:
index.<hash>.css → 851 KB raw → 113.5 KB gzip
…cargado en toda página /uix porque el layout importa
@/uix/eidos/index.css.
Causa raíz: agregado monolítico
src/uix/eidos/index.css hace @import de 104 de 109 componentes:
| Componentes | |
|---|---|
Auto-importan su .css desde el .svelte |
10 (badge, card, image, password-field, s-text, s-text-virtual-list, scroll-frames, skeleton, spinner, textarea) — antes también en index.css ⇒ doble-carga; quitados del agregado en Fase 0 |
Agregados en index.css |
104 → 94 tras Fase 0 |
CSS de componentes alcanzable SOLO vía index.css:
723 KB raw ≈ 94 KB gzip
Una app que renderice 3 componentes paga el CSS de los 104. Eso es más peso que todo el JS de la página junta, y casi todo es CSS de componentes que nunca se montan.
Desglose del stylesheet
| Pieza | raw |
|---|---|
generated/base.css (escalas de color × roles × light/dark) |
337 KB |
| 104 CSS de componente agregados | 723 KB (resto del archivo tras dedupe de @import) |
words.css (el más pesado individual) |
55 KB |
date-range-picker, color-picker, drawer, range-calendar… |
18–25 KB cada uno |
Comparativa con otros frameworks
⚠️ Cifras de referencia públicas, no medidas en este repo. La comparación exacta es imposible (distinto scope, distinto modelo de estilado), pero sirve para situar el orden de magnitud. Lo que importa es el modelo de distribución, no el número absoluto.
JavaScript (runtime + un componente típico, gzip aprox.)
| Framework | Modelo | JS por componente (gz) | Notas |
|---|---|---|---|
| activeUIX (soma) | Svelte 5, headless + eidos | ~7 KB (mediana) + ~35 KB runtime compartido | Runtime Svelte se amortiza; componentes code-split |
| Radix UI (primitives) | React, headless | ~10–20 KB por primitiva + React (~45 KB) | Tree-shakeable por paquete (@radix-ui/react-*) |
| Chakra UI v2 | React, styled-system | Pesado: runtime emotion + theme (~100 KB+ base) | Runtime CSS-in-JS; conocido por bundle grande |
| Chakra UI v3 / Ark UI | React, headless (Zag.js) | ~15–30 KB por componente + state machines | Más ligero que v2 |
| MUI (Material UI) | React, emotion | ~80–150 KB base + emotion runtime | El más pesado de la lista |
| shadcn/ui | Copia código a tu repo | Solo lo que pegas (~5–15 KB) | No es dependencia; cero runtime propio |
| Bits UI / Melt | Svelte, headless | ~5–10 KB por componente | Comparable a activeUIX |
Lectura: en JS, activeUIX juega en la liga ligera (Svelte + headless + code-split), comparable a Bits UI / Radix y muy por debajo de Chakra v2 / MUI. El runtime de Svelte (~35 KB) se amortiza a partir de un par de componentes y no crece con React-sized overhead.
CSS / estilado (el eje donde difiere el modelo)
| Framework | Modelo de CSS | Coste típico |
|---|---|---|
| activeUIX (eidos) hoy | Stylesheet agregado global | 113 KB gz fijo en toda página ⚠️ |
| activeUIX (eidos) propuesto | Foundation + CSS por componente | ~50 KB foundation + lo que uses |
| Radix Themes | Stylesheet global (@radix-ui/themes/styles.css) |
~30–45 KB gz (todo el sistema) |
| Chakra v2 | CSS-in-JS (emotion), runtime | 0 KB estático, pero coste JS/runtime alto |
| Tailwind (shadcn) | Utilidades purgadas | ~5–15 KB gz tras purge (solo clases usadas) |
| MUI | CSS-in-JS (emotion) | Runtime, similar a Chakra |
| Bits UI | Sin estilos (BYO) | 0 KB (tú pones el CSS) |
Lectura: el modelo "stylesheet global" no es malo per se — Radix Themes hace lo mismo. El problema es la magnitud: el agregado de activeUIX (113 KB gz) es ~2.5–3× el de Radix Themes (~40 KB) porque incluye los 104 componentes sin posibilidad de cargar solo un subconjunto. Tailwind/shadcn ganan en CSS precisamente porque purgan lo no usado — que es exactamente lo que propone la recomendación nº 1.
Recomendaciones (priorizadas)
1. Romper eidos/index.css en CSS por-componente — alto impacto, esfuerzo medio
Que cada *.svelte importe su propio .css (como ya hacen 10 componentes).
Vite los code-splittea automáticamente y la app solo carga el CSS de lo que
renderiza. index.css quedaría solo con la foundation: generated/base.css
archetypes.css+events.css+ primitivas de layout (box/flex/grid/stack…).
Efecto esperado: convierte "113 KB fijos siempre" en "foundation (~50 KB) + CSS de los componentes que montas". Una app de 5 componentes pasaría de 113 KB → ~55–65 KB gz de CSS.
Plan seguro (probar uno, verificar, propagar):
- Migrar un componente (p. ej.
dialog): añadirimport './dialog.css'en su.svelte, quitar su@importdeindex.css. - Build + verificar en navegador que el estilo sigue intacto y que el CSS de dialog ya NO está en el stylesheet global (sale como chunk propio).
- Medir el delta. Si cuadra, propagar a los 103 restantes.
Riesgo a vigilar: componentes que portalizan contenido (popover/dialog/drawer/ menús) — sus tokens custom no heredan a través del portal. La foundation debe seguir conteniendo los tokens globales que esos descendientes portalizados necesitan (ver feedback
cross_portal_css_vars).
2. Auditar sistemas de calendario no-gregorianos — medio impacto
~67 KB gz repartidos en 2 chunks incluyen gregorian/islamic/buddhist/japanese/
persian/ethiopic. Si la mayoría de apps solo usan gregoriano, cargarlos por
dynamic import por sistema desde $libs/days recorta ese peso de las páginas de
fecha/calendario.
3. No tocar el JS de soma/sema/color — ya está bien
Code-split y tree-shakeado correctamente. No sobre-optimizar.
Apéndice — comandos de medición
npm run build
# Total cliente
du -sh build/_app/immutable
# JS vs CSS raw
find build/_app/immutable -name '*.js' -printf '%s\n' | awk '{s+=$1} END{print s/1024" KB JS"}'
find build/_app/immutable -name '*.css' -printf '%s\n' | awk '{s+=$1} END{print s/1024" KB CSS"}'
# El stylesheet monolítico, raw + gzip
for f in $(find build/_app/immutable -name '*.css' -printf '%s %p\n' | sort -rn | head -1 | awk '{print $2}'); do
echo "raw $(stat -c%s "$f") gzip $(gzip -c "$f" | wc -c)"
done
# Self-import vs agregado
grep -rl "import '\./.*\.css'" src/uix/eidos/components --include='*.svelte' | wc -l
grep -c "@import './components" src/uix/eidos/index.css