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

302 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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): import en la raíz · tokens cross-portal OK · **`@layer` descartado** | medio | hecho — ver resultado |
| **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.
### Decisión `@layer` (Fase 1) — DESCARTADO
Evaluado con evidencia, no en teoría. **No se adopta `@layer`**:
- **0** usos actuales en eidos → sería un sistema nuevo sobre las 104 recipes.
- **16** `!important` en 5 recipes → `@layer` invierte su precedencia (en cascada
con capas, `!important` resuelve en orden de capa **inverso**) ⇒ regresiones
silenciosas a auditar una a una.
- Cada recipe ya scopea a `[data-{component}]` (doctrina CLAUDE.md) → las
colisiones cross-componente a igual especificidad están **estructuralmente
prevenidas**. La única dependencia de orden real (`menu-indicator`, partial
compartido) se resuelve dejándolo en foundation (Fase 2).
El scoping por `data-*` ya da el determinismo que daría `@layer`, sin su coste
(envolver 104 ficheros) ni su riesgo (invertir 16 `!important`). `@layer` queda
como **escape hatch documentado** si algún día aparece una colisión que el
scoping no resuelva.
### Fase 1 — resultado medido (piloto `dialog`)
- **Patrón fijado para compuestos**: el `import './x.css'` va en el `.svelte`
**raíz** (`dialog.svelte`). El barrel (`index.ts`) importa la raíz, así que el
CSS carga al usar cualcomponente parte (Trigger, Content portalizado, …).
- `index.css`: 94 → **93** `@import`. Monolito **814 420 → 805 692 raw**
(−8.7 KB) · **108 991 → 107 843 gz** (−1.1 KB).
- Recipe fuera del monolito: `[data-dialog-content]` / `[data-dialog-overlay]`
= **0**; viaja en su chunk. El token `--dialog-content-bg` se queda en
`base.css` (global).
- **Cross-portal verificado en navegador**: el `[data-dialog-content]`
portalizado (fuera de `[data-dialog]`) sigue **completamente estilado**
(bg `oklch(0.285 0 0)`, radius 16px, sombra, padding 20px) — tokens de
`base.css` global + recipe del chunk. El portal no rompe nada.
- `check` 0 errores.
## 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
```bash
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.