Skip to content

Pantallas esqueleto: la UI espera con la forma de lo que va a llegar - #17

Merged
alvarotorresc merged 6 commits into
mainfrom
feat/skeleton-screens
Aug 31, 2026
Merged

Pantallas esqueleto: la UI espera con la forma de lo que va a llegar#17
alvarotorresc merged 6 commits into
mainfrom
feat/skeleton-screens

Conversation

@alvarotorresc

@alvarotorresc alvarotorresc commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Las pantallas cambiaban de tamaño al cargar y cada una esperaba distinta. La causa era una sola: las dieciocho rutas esperaban con la misma pokébola centrada dentro de 60px de padding, así que el hueco no medía nada parecido a lo que iba a ocupar el contenido, y una rejilla, una tabla y una ficha esperaban las tres igual para aparecer luego con tres formas distintas.

Ahora cada ruta espera con un esqueleto que tiene la forma y el alto de lo que va a pintar.

Lo que se midió

En local todo carga instantáneo y no reproduce nada, así que todo esto está medido con red lenta simulada (Fast 3G: 1,6 Mbps, 150 ms de latencia, CPU ×4). Antes, el spinner estaba en pantalla entre 0,7 y 3,5 segundos según la ruta.

Salto de altura al llegar los datos, antes y después:

ruta antes después
#/pokedex +3058 px 0
#/moves +3025 px +74
#/abilities +2699 px 0
#/meta +1045 px 0
#/items +748 px +110
#/pokedex/<id> ficha entera 0
#/counter #/survive #/speed #/compare #/team #/egg ya era 0 0

Verificado también en tema claro (Pokédex 0, movimientos 74, ficha 40) y a 380px de ancho (Pokédex +156). Sin desbordes horizontales en ninguna.

Cómo está hecho

skeletonHTML() en js/ui.js es la única función, con seis formas para las dieciocho rutas: grid, tiles, cards, table, blocks y detail. No hay un esqueleto por pantalla, así que no hay donde puedan separarse.

El esqueleto reutiliza las clases reales del contenido que sustituye.pokemon-grid con .pokemon-card, .items-grid, .data-table, y el .bento de la ficha. Esa es la propiedad que importa a un año vista: el hueco mide lo que va a medir el contenido sin repetir aquí ni un tamaño, y cambiar el alto de una tarjeta mueve su esqueleto solo. Donde hay paginación, cada ruta pasa su propio PAGE_SIZE (50 la Pokédex, 48 objetos, 30 habilidades).

Sin temporizador en JS. El esqueleto entra en el DOM siempre, porque reservar el hueco desde el primer frame es justo lo que quita el salto, pero la animación lo tiene invisible 200 ms: con los datos en caché el contenido lo sustituye antes de que llegue a verse, y una espera corta ya no parpadea en gris.

Los sprites entran en vez de aparecer de golpe. Medido: la rejilla se pinta con sus doce sprites visibles a cero cargados y tardan 364 ms más en llegar. No era un problema de layout (el hueco ya estaba reservado, .sprite son 96/128px fijos), sino el golpe. Un solo listener en captura filtrando por /sprites/, y el estado por defecto es visible: si no llega a dispararse, el sprite se ve sin animación en vez de quedarse en blanco.

La pokébola se queda sin llamantes y se va con sus 43 líneas de CSS. Las claves de i18n siguen vivas y ahora las anuncia el lector de pantalla, que con el esqueleto solo se quedaba sin nada que decir.

Lo que no llega a cero, y por qué

  • La tabla de movimientos no puede: su fila no tiene alto fijo, va de 58,2 a 85,5px en la primera página y sube a 83,2 de media en la doce. Se ajusta a la primera, que es la única que llega a ver un esqueleto (el hueco lo pinta loadAll(), que corre una vez).
  • La ficha en móvil conserva unos 551px de salto sobre 4751. No es el hueco de la ficha, que ya está ajustado: es una de las secciones que se rellenan solas dentro de ella (línea evolutiva o movimientos), cuyo esqueleto sigue dimensionado para escritorio. Queda anotado como seguimiento.
  • Los ~0,9 s iniciales de página vacía, antes de que arranque el router, no los arregla ningún esqueleto: eso pide prerenderizar la cabecera en el build, que ya está en los seguimientos y se deja fuera a propósito.

Verificación

  • npm run check: los 26 checks en verde.
  • npm run build: OK, 53 módulos, CSS con las clases nuevas.
  • Medido en el navegador con red lenta, en las dos tintas y a 1280 y 380 de ancho.

La espera era la misma pokebola centrada en las dieciocho rutas, y de ahi
salian las dos cosas que se ven mal. El salto: medida a Fast 3G la Pokedex
espera en 1199px de alto y aterriza en 4257, 3058px que se abren de golpe
bajo el cursor (movimientos +3025, habilidades +2699). Y la mezcla: con el
hueco identico en todas, una rejilla, una tabla y una ficha esperaban con la
misma forma y aparecian con tres distintas.

skeletonHTML() reutiliza las clases reales del contenido que sustituye, asi
que el hueco mide lo que va a medir el contenido sin repetir aqui ni un
tamano. Cinco formas para las dieciocho rutas, una sola funcion: no hay donde
puedan separarse.

Sin temporizador en JS. El esqueleto entra en el DOM siempre -- reservar el
hueco desde el primer frame es lo que quita el salto -- pero la animacion lo
tiene invisible 200ms, asi que con los datos en cache el contenido lo
sustituye antes de que llegue a verse y una espera corta ya no parpadea.
Cada ruta pide la forma de lo que va a pintar y, donde hay paginacion, le
pasa su PAGE_SIZE en vez de un numero escrito otra vez aqui: la Pokedex son
50, los objetos 48, las habilidades 30. Es lo que hace que el hueco mida lo
que va a medir el contenido sin que nadie tenga que acordarse de nada.

Las de calculadora no cambian de forma -- el formulario ya se ve al instante
y solo espera el panel de resultado -- asi que ahi el esqueleto es ese panel
y no la pantalla entera.

Con esto la pokebola centrada se queda sin llamantes: fuera ella, su CSS y
sus 43 lineas de spinner. Las claves de i18n siguen vivas y ahora las dice
el lector de pantalla, que con el esqueleto solo se quedaba sin nada que
anunciar.
Las tres formas que no clavaban el alto, con el numero que dio medirlas:

- La tabla de movimientos calculaba la fila desde el padding de .data-table
  td y salian 28px cuando la real mide 58,2 de minimo -- quien manda dentro no
  es el texto, es el badge de tipo. A 50 filas eso dejaba 1613px de salto.
- El cuerpo de meta mide 1719px y esperaba con 408.
- La ficha de un Pokemon mide 2073 y esperaba con 608.

El bloque sube de 90 a 120px: la ficha con bloques bajos pedia 18 barras
grises seguidas, que son mas ruido del que quitan, y a 120 son 14 y ademas se
parecen a lo que va a haber ahi, que son tarjetas de seccion.

La tabla es el unico hueco que no sale exacto y no puede salir: su fila no
tiene alto fijo, va de 58,2 a 85,5 en la primera pagina y sube a 83,2 de
media en la doce. Se ajusta a la primera, que es la unica que llega a ver el
esqueleto -- el hueco lo pinta loadAll(), que corre una vez.

Medido a Fast 3G, el salto al llegar los datos:
Pokedex 3058 -> 0, habilidades 2699 -> 0, meta 1045 -> 0, objetos 748 -> 110,
ficha 172, movimientos 3025 -> 474 y con esto ~10.
El esqueleto arregla el hueco, no lo que lo rellena. Medida a Fast 3G, la
rejilla de la Pokedex se pinta con sus doce sprites visibles a cero cargados,
y tardan 364ms mas en llegar: el sitio ya estaba reservado --.sprite es 96 o
128px fijos en CSS y ninguno empuja nada-- asi que no habia salto, pero si
doce cuadros apareciendo de golpe sobre un hueco vacio.

Un unico listener en captura y no un onload por <img>: `load` no burbujea, y
los sprites se pintan desde ocho plantillas distintas que tendrian que
acordarse cada una. El filtro es la ruta, que es lo que los define de verdad:
todos salen de spriteUrl() o itemSprite() y viven bajo /sprites/.

El estado por defecto es visible y no transparente a proposito. Si el
listener no llega a dispararse, el sprite se ve sin animacion; al reves, un
fallo aqui dejaria la Pokedex en blanco.
La ficha de un Pokemon no es una columna de bloques: es .bento, ocho
secciones repartidas por column-count. El esqueleto pintaba una sola columna,
asi que se veia bien hasta que llegaba el contenido y la pagina cambiaba de
forma -- justo lo que esto venia a quitar. Ahora reutiliza .bento y .b, que
es lo que hace que tenga las mismas columnas que la ficha en cada ancho sin
repetir aqui un solo breakpoint.

De paso, un fallo mio: la altura de seccion la puse como `.b .sk-block`, y
las secciones que se rellenan solas dentro de la ficha (linea evolutiva,
movimientos) pintan su esqueleto DENTRO de un .b -- esa regla les subia el
bloque a 346px sin que nadie se lo pidiera. Va con clase propia.

La linea evolutiva mide 527px y esperaba con 264. Con tres bloques en vez de
dos, y el resto ya medido, el salto de la ficha entera se va a 0: medido a
Fast 3G en Pikachu y en Mewtwo, que no tiene cria.
Por debajo de 1000px column-count vuelve a 1 y las ocho secciones se apilan
en vez de repartirse: la ficha entera pasa a medir 4751px. El hueco seguia
calculado para dos columnas y se quedaba en 3486. Con 425 por seccion sube a
4039, medidos los dos a 380 de ancho.
@netlify

netlify Bot commented Aug 31, 2026

Copy link
Copy Markdown

Deploy Preview for fluffy-pasca-8ba512 ready!

Name Link
🔨 Latest commit 7af4c4b
🔍 Latest deploy log https://app.netlify.com/projects/fluffy-pasca-8ba512/deploys/6a958ac5cb2fb80008bbaf0b
😎 Deploy Preview https://deploy-preview-17--fluffy-pasca-8ba512.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@alvarotorresc
alvarotorresc merged commit ce54d72 into main Aug 31, 2026
5 checks passed
@alvarotorresc
alvarotorresc deleted the feat/skeleton-screens branch August 31, 2026 19:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant