ADR-019 — Preact (via preact/compat) en la landing¶
Status: accepted (operador 2026-08-22 — enmienda a ADR-015)
Context: docs/E2E-PLAN.md A9 fija el objetivo de performance de la landing: la
Pantalla 1a visible en menos de 1.5 s bajo el perfil "3G lento" del e2e (99% de la
audiencia real entra desde el celular, operador: "de eso depende que lo usen"). Con el
estado inicial embebido en el HTML y los assets precomprimidos (brotli/gzip) ya en
producción, el bundle inicial medía ~51 KB gzip / ~45 KB brotli — de los cuales el
runtime de React (react + react-dom) representaba ~45 KB, la mayor parte del peso.
El código propio de la landing (componentes, hooks, ErrorBoundary de clase) pesa unos
pocos KB; el costo real era el framework, no el producto.
Alternatives:
Quedarse en React 18.3 — cero riesgo de compatibilidad, pero el runtime sigue
pesando ~45 KB gzip sin margen para bajar del objetivo de 1.5 s en 3G real.
Preact vía preact/compat (elegida) — Preact (~4 KB) implementa la misma API de
React (hooks, componentes de clase, Context, refs) a través de la capa de
compatibilidad preact/compat; el código fuente no cambia, solo el runtime que lo
ejecuta. @preact/preset-vite alias react/react-dom/react/jsx-runtime a
preact/compat/preact/jsx-runtime en build time.
Reescribir en vanilla JS u otro framework más chico — descartado: reescritura
completa de 8 pantallas + validaciones + escaneo de DNI para un ahorro similar al de
la opción 2, con mucho más riesgo y sin reutilizar la suite de tests existente.
Decision: Adoptar Preact vía preact/compatsolo en la landing (landing/). El
gestor (gestor/, SPA interna de uso operativo, sin presupuesto de peso — ver ADR-015)
se queda en React 18.3 sin cambios: no hay presión de 3G ni de audiencia masiva ahí.
landing/package.json: preact como dependencia; @preact/preset-vite reemplaza a
@vitejs/plugin-react como devDependency (mismo soporte JSX/Babel + HMR, y ya trae el
alias react→preact/compat resuelto en vez de declararlo a mano). Los paquetes runtime
react/react-dom (18.3.1) se DESINSTALARON — con el alias, nada los ejecuta nunca;
@types/react/@types/react-dom alcanzan solos para que tsc resuelva los tipos
(confirmado: tsc --noEmit sigue en verde sin los paquetes runtime instalados).
El código fuente de la landing (componentes, hooks, ErrorBoundary) sigue escrito
contra la API de React tal cual — ningún import cambia salvo la infraestructura de
test (ver Consequences).
Consequences:
Bundle inicial: 51.33 KB → 14.29 KB gzip (12.89 KB brotli) — el runtime de
React/ReactDOM (~45 KB) se reemplaza por Preact (~4 KB); el código propio de la
landing (~9-10 KB) queda intacto.
API de React intacta: hooks, Component de clase (ErrorBoundary),
createRoot/StrictMode, refs y el mapeo onChange→evento de input de los
formularios (DNI/celular/etc.) funcionan igual — cubierto por la suite existente sin
reescribir ningún test de comportamiento.
Riesgo de incompatibilidades menores de preact/compat: mitigado por (a) un
relevamiento previo del código fuente que confirmó que la landing NO usa
React.lazy/Suspense (el escaneo de DNI carga su chunk pesado con un import()
dinámico manual, sin la API de lazy-loading de React), useId, createPortal,
forwardRef ni useImperativeHandle — superficie de compat conocida como más frágil
que no aplica acá; y (b) la suite de vitest (51 tests) + el e2e completo corriendo en
verde tal cual, sin ajustar ningún assert de comportamiento.
Infraestructura de test, sí cambió:@testing-library/react asume react-dom
real — internamente hace require("react-dom")/require("react-dom/client") como
CommonJS, y Vitest ejecuta esos requires de dependencias de node_modules con el
require nativo de Node (no pasan por el resolver de Vite), así que ni
resolve.alias ni vi.mock los interceptan de forma confiable — terminaba montando
los elementos de Preact (ya vía JSX transformado por @preact/preset-vite) con el
reconciler REAL de react-dom (dos runtimes distintos en el mismo árbol: "Objects are
not valid as a React child", refs inválidos). Se reemplazó por
@testing-library/preact (adaptador oficial, misma API render/screen/cleanup/
act, implementado directamente sobre Preact) en los 6 archivos de test que
renderizaban componentes — sin tocar ningún assert de comportamiento.
Gestor no cambia: stack, bundle y tests del gestor quedan exactamente igual que
antes de este ADR.
landing/vite.config.ts: preact() en vez de react() en plugins.
landing/tests/setup.ts y los 6 archivos de test que renderizan componentes:
@testing-library/react → @testing-library/preact.
Medir bundle (gzip + brotli) y el test "3G lento" (e2e, 3 corridas) antes/después;
ajustar el umbral del test (docs/adr/../E2E-PLAN.md A9) al valor medido honesto.