Saltar a contenido

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/compat solo 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.
  • Implementation Plan:
  • landing/package.json: agregar preact; reemplazar @vitejs/plugin-react por @preact/preset-vite; agregar @testing-library/preact; quitar @testing-library/react.
  • 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.
  • Verification:
  • [x] landing/dist gzip inicial ≤ 60 KB (AC-24) — 14.29 KB, con amplio margen.
  • [x] Suite vitest de landing (51 tests) en verde sin reescribir asserts de comportamiento.
  • [x] tsc --noEmit y eslint . de landing sin errores.
  • [x] E2E completo (Playwright, jar empaquetado) en verde.
  • [x] Gestor sin cambios de stack, bundle ni tests.