Saltar al contenido
Sistemas
2026-06-089 min lecturaEn producción

PokeCore — 1025 Pokémon con cero latencia.

Cómo renderizar miles de elementos manteniendo 60 FPS. Caché en cascada, prefetching inteligente y virtualization que no bloquea.

React 19ViteTanStack QueryFramer Motion
PokeCore - 1025 Pokémon interface

Hace años, las Pokédex web eran simples: carga un JSON, renderiza un grid y espera que el navegador aguante. Hoy la expectativa es la contraria: que una web con 1025 Pokémon devuelva resultados antes de que termines de escribir. PokeCore parte de ahí, y la pregunta de diseño no fue "cómo muestro todos" sino "cómo hago que parezca que no hay tantos".

Caché en cascada: reducir network a casi cero

Hay tres niveles de caché en PokeCore, y la separación es deliberada. El primero es localStorage: cuando abres la app, el navegador ya sabe qué Pokémon marcaste como favoritos la última vez, sin tocar la red. El segundo es TanStack Query, que cachea los datos que ya pediste mientras navegas entre regiones y no los vuelve a pedir. El tercero es la propia capa HTTP: PokéAPI es lenta desde algunas regiones, pero solo se consulta una región a la vez.

La pregunta clave fue cuándo precargar el resto, y la respuesta es en el momento en que abres la app. Mientras ves la pantalla de inicio (el selector de región), Query ya está pidiendo en segundo plano los datos de Kanto, así que para cuando haces clic ya está todo cargado. Esto exige entender el flujo del usuario antes de programar: no se trata de precargar todo, sino de precargar lo que el usuario probablemente querrá ver.

Virtualization: renderizar lo que ves, no lo que existe

1025 Pokémon son demasiados para renderizar. Pero también son innecesarios: si el grid tiene 4 columnas y caben 8 filas en pantalla, solo necesito 32 elementos renderizados. Los otros 993 pueden esperar.

La técnica se llama virtualización: conforme haces scroll, las tarjetas que salen de pantalla se destruyen y las nuevas se renderizan, sin que el usuario lo perciba. Las animaciones siguen siendo suaves porque Framer Motion no bloquea el hilo principal, y el resultado es que, incluso en móvil, navegar 1025 Pokémon va tan rápido como navegar 50.

Tipos dinámicos: 18 colores, un solo sistema

Cada Pokémon tiene un tipo o dos. Agua, Fuego, Tierra, Psíquico... son 18 tipos totales, cada uno con un color específico.

La solución fácil era hardcodear 18 valores de color en CSS; la correcta, pasarlos como tema a Tailwind en compilación. Eso me permite dos cosas: los colores tienen una fuente única (si mañana quiero un verde menos agresivo, cambio un número y todos los Pokémon de tipo Planta se actualizan) y aparecen coherentes en pills, fondos y glows sin duplicación. No es un detalle cosmético: es un sistema de colores que responde a datos.

Responsive sin "breakpoints": escala fluida

El grid tiene 4 columnas en desktop, 3 en tablet, 2 en móvil. Algunos cambios necesitan un breakpoint. Pero la mayoría debería ser fluida: si tienes 375px de ancho, esperas 2 columnas. Si tienes 390px, también. El paso de 3→2 debería ser invisible.

Tailwind tiene grid-cols-auto-fit — un feature CSS nativo que calcula el número de columnas automáticamente basado en el ancho disponible y un mínimo de columna. Sin breakpoints. Sin decisiones arbitrarias. Solo CSS.

Modal glass con fallback en cascada

Cuando haces clic en un Pokémon, el modal muestra una imagen que viene de PokéAPI con tres fuentes posibles: official-artwork (la mejor), home (aceptable) y front_default (básica). Si official-artwork no existe se carga home, si home tampoco se carga front_default, y si no hay ninguna se renderiza un placeholder.

Esta lógica vive en un componente SpriteImg reutilizable. En lugar de que cada pantalla maneje sus propios fallbacks, la responsabilidad queda en un solo sitio: el componente renderiza una imagen de Pokémon con sus fallbacks, y quien lo usa no tiene que preocuparse de eso.

Favoritos: síncrono en cliente, persistente sin servidor

Los favoritos se guardan en localStorage, sin tocar servidor: el cambio es instantáneo y persiste entre sesiones, así que puedes cerrar la pestaña y encontrarlos mañana. Zustand maneja el estado global: al marcar un favorito actualiza la memoria y localStorage a la vez, y el sidebar se renderiza directamente desde ese estado.

La complejidad que evité fue la sincronización con servidor, el login y la autenticación. Los favoritos no necesitaban nada de eso: localStorage es suficiente, y añadir infraestructura que el caso de uso no pide solo habría sumado mantenimiento.

Lo que aprendí construyendo PokeCore

La pregunta inicial era si podía hacer una Pokédex web completa, y la respuesta fue que sí, pero no a base de agregar features, sino de entender dónde estaba la fricción: red lenta, renderizado de demasiados elementos y decisiones de diseño sin sistema. Cada decisión resolvía un problema concreto: prefetch porque la red es lenta, virtualización porque el navegador no puede renderizar 1025 elementos, caché en cascada porque cada milisegundo cuenta en móvil. No es una aplicación compleja, sino una aplicación simple con decisiones deliberadas en cada capa.

Construido porIsmael Manzano LeónFull Stack Developer · leo/ · leosoftware.dev
Abrir PokeCore