You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/optimize-bundle.md

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). Branch active-uix. Todas las cifras "medidas" salen de build/_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 de words.css. Build verde, sin acoplamiento roto.

Metodología

  • npm run build → adapter-static, salida en build/.
  • 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 solo Soma y 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/oklch NO 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):

  1. Migrar un componente (p. ej. dialog): añadir import './dialog.css' en su .svelte, quitar su @import de index.css.
  2. 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).
  3. 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

Powered by TurnKey Linux.