loading...
Cover image for Dejar las cosas quietas: parte 1

Dejar las cosas quietas: parte 1

patopitaluga profile image Patricio Pitaluga ・10 min read

Animar los contenidos web con css es fantástico, pero sabés qué cosa es mejor: dejarlos quietos. Más específicamente dejarlos quietos cuando el usuario va a interactuar con ellos.

No es un argumento en contra de la animación (que los hay) sino a favor de la usabilidad. Los usuarios son somos ansiosos e internet no es tan rápida como nos gustaría. Hay muchos sitios que hacen aparecer y desaparecer elementos en pantalla (te hablo a tí, Twitter👈🤨) justo en el momento en el que el usuario va a interactuar con (o intentar leer) lo que estaba en ese lugar antes, trayendo con esto likes involuntarios, peleas de parejas, confusiones y fastidio.

En acción:


¿Pero… qué tan rápido puede ser el usuario?

Los usuarios NO van a querer interactuar antes de que esté cargado completamente el sitio… ¿No?

Este es un jugador profesional de Overwatch entrenando su velocidad de respuesta:
Un jugador de Overwatch disparando rapidísimo
Creo que puedo pasar una pantalla de login mucho más rápido 😆.


¿Cuándo sucede?

El contenido html, si es de un tamaño razonable, se renderea en el navegador prácticamente en el acto. Una vez que esto sucedió, el usuario ya puede empezar a interactuar, por ejemplo, intentar clickear un enlace o leer un texto.

Los estilos css y el código javascript pueden ser render blocking o no, es decir, pueden permitir que los elementos subsiguientes se muestren aunque el asset en cuestión no esté completamente cargado. El developer debe elegir estratégicamente qué elementos serán render blocking y cuáles no, para dar la mejor experiencia al usuario desde el segundo cero y evitar mover el contenido que ya es interactuable.

En todo caso, las imágenes en elemento <img> no son render blocking por default y tampoco las tipografías (estas dos cosas son, seguramente, los assets más pesado del sitio).

También, en webapps (sobre todo con frameworks javascript de contenido reactivo) se suele cargar información con XMLHttpRequests (a veces llamado ajax) para que en una lista de elementos se inserten elementos más nuevos arriba, desplazando a los anteriores.

Muchas veces, desde el diseño, no se consideran espacios para notificaciones para que no sean obstáculo para el resto de los contenidos.

Algunos casos en los que el contenido se puede mover inesperadamente mientras el usuario quiere interactuar:

  • Cuando se terminan de cargar imágenes y desplazan el contenido vecino.
  • Cuando se terminan de cargar tipografías y cambian el tamaño de los elementos en los que se encuentran y los elementos vecinos.
  • Cuando se muestran nuevos elementos con información cargada con XMLHttpRequests y desplazan los elementos previos.
  • Cuando no se tuvo una estrategia de render blocking para el css o javascript y se redimensionan elementos al cargar estilos.
  • Cuando se usa un framework javascript y no se utilizó cloak para ocultar el contenido template mientras carga.
  • Cuando se muestran notificaciones en pantalla al usuario que quedan por encima de elementos interactivos o textos.

¿Cómo evitarlo?

En este artículo solo voy a plantear las soluciones que se me ocurren para resolver problemas con carga de imágenes. Atacaré las otras posibles causas en siguientes posts.

1 Posibles soluciones para evitar desplazar elementos al cargar imágenes

Ya sea porque se está utilizando lazy loading, porque la imagen es muy pesada o porque el proveedor de internet está perezoso (hablo de tí, Fibertel👈🤨), las imágenes pueden tardar unos segundos en cargar. Cuando un elemento <img> no terminó de cargar la imagen de su src, su tamaño es cero width y cero height por lo que no desplazará los elementos adjuntos. En la medida que los cargue, "crecerá" y ocupará su espacio. Para evitar desplazar elementos al cargar imágenes se puede:

1.1 Establecer las propiedades width y height del propio elemento.

Sí. Sin css, como hacía tu abuelo 😆. Ventaja: el tamaño será rendereado inmediatamente sin importar dónde se carguen los estilos css Desventaja: como se setea un único tamaño no es responsive.

<img alt="..." src="some-image.jpg" width="100" height="50"/>
<p>Este párrafo no será desplazado.</p>

Es una solución primitiva para un problema primitivo y muchas veces es suficiente.

Nota: No se debe especificar que son px como se haría en css porque por defecto considera que el valor son píxeles. También se puede setear valor porcentual con el signo %.

1.2 Setear el width y height de la imágen explícitamente en la hoja de estilo

Es la solución más óptima. Ventajas: funciona estupendamente, es responsive y permite usar imágenes de mayor definición para pantallas con alta densidad de pixeles (retina) Desventajas: hay que conocer tamaño de cada imagen y especificarla en la hoja de estilo. El bloque <style> deberá estar antes o inmediámente después del elemento <img> ya que si se carga asincrónicamente pueden ocurrir unos segundos mientras el browser renderea el alto del elemento de su estilo.

<style>
.the-image {
  height: 100px;
  width: 100px;
}
@media only screen and (min-width: 768px) {
  .the-image {
    height: 150px;
    width: 150px;
  }
}
</style>
...
<img alt="..." class="the-image" src="my-image.jpg"/>
<p>Este párrafo no será desplazado.</p>

1.3 Reservar el espacio en el elemento contenedor

Se puede establecer el height del estilo, no de la imagen en sí, sino del elemento contenedor. Luego la imagen puede estar en un elemento <img> dentro, o bien como background-image del contenedor con background-size: cover o contain. Desventaja: con esto se decide un alto máximo pre-establecido y las imágenes que sean de una proporción más vertical quedan cortadas.

1.4 Reservar el espacio relativo a la proporción de la imagen

¿Y qué pasa si no sé cuánto va a medir la imagen de alto porque está en un elemento responsive que ajusta su ancho?

Seguramente sí conozcas su proporción (aspect ratio).

Considerando que el padding porcentual es relativo al ancho del elemento, se puede setear la proporción como padding-bottom del contenedor. Por ejemplo, para que una imagen que sabemos que será cuadrada, ya tenga el alto correcto antes de cargarse, se puede contener con un elemento con padding-bottom: 100% y con la imagen como position: absolute. Esto también es útil cuando deba ponerse una imagen como background-image. Otros porcentajes se pueden calcular con regla de tres simple pero es útil tener a mano las dos proporciones de fotos más usadas: 16:9 es el 56.25% y 4:3 es el 75%. Ventajas: es responsive. Desventajas: es un hack y obliga a poner la imagen como position absolute. Ejemplo:

<style>
  .my-img-container {
    padding-bottom: 56.25%; /* 16:9 aspect ratio */
    position: relative;
  }
  .my-image {
    position: absolute;
  }
</style>
...
<figure class="my-img-container">
  <img alt="" src="my-image-with-known-aspect-ratio.jpg" class="my-image"/>
</figure>
<p>Este párrafo no será desplazado.</p>

Hay un feature experimental de css que permite directamente específicar el aspect ratio y esto dejará obsoleto el hack del padding proporcional, pero todavía (agosto 2020) no es soportada por muchos navegadores. En el futuro será:

.mi-image {
  aspect-ratio: 16 / 9;
}

Si estás leyendo esto mucho más adelante podés checkear: https://caniuse.com/#search=aspect-ratio para saber si ya está aconsejado su uso.

1.5 En grillas de thumbnails considerar el espacio que ocupará la imagen de mayor alto

Suele suceder mucho en tiendas online que se plantea una grilla de items con thumbnails. Si todas esas fotos tendrán la misma proporción, se puede aplicar la solución 1.2 especificando el alto en css pero, muy probablemente, como desarrolladores permitamos que los usuarios suban imágenes de cualquier proporción. En ese caso se pueden pensar varias soluciones:

  • La solución 1.3 antes mencionada: especificar por css un alto máximo y que todas las imágenes se encuentren centradas verticalmente en contenedores de ese alto Desventaja: quedan espacios en blanco arriba y abajo en las imágenes más apaisadas.
  • Con css forzar a que todos los elementos de ese row de la grilla se adapten al alto del de la imágen de mayor altura con grid-auto-rows: 1fr; o con display: flex al contendor y flex: 1 1 auto a los contenidos Desventaja: de todos modos se desplazarán los contenidos mientras no se haya cargado la imagen más alta.

1.6 Precargar imágenes que se mostrarán luego para reservar su alto

Como el elemento <img> puede no estar presente en el DOM inicialmente sino que se muestra con javascript, puede ser útil precargar las imágenes. Se puede hacer con css pero si se hace con javascript ya sabremos con anticipación el tamaño y la proporción de la imagen antes de mostrarla. En ese caso se puede setear el height en el style del elemento antes de mostrarlo. Ventaja: como se carga por javascript, el elemento <img> puede estar display: none o incluso ausente en el DOM. Desventaja: si no se ocultan todos los contenidos hasta haber cargado la imagen, de todos modos se renderea inicialmente con alto cero y se desplaza el resto de los contenidos terminar la precarga. Ejemplo en vanilla js para que se comprenda. Puede reproducirse de manera similar en React, Angular o Vue:

  <style>
  img {
    display: block;
  }
  .tab {
    display: none;
  }
  .tab.tab--active {
    display: block;
  }
  </style>

  <button onclick="showTab(1)">Ver solapa 1</button>
  <button onclick="showTab(2)">Ver solapa 2</button>

  <div id="tab1" class="tab tab--active">
    <p>Aquí no hay ninguna imagen, por lo que el alto será el mínimo</p>
    <div id="image-placeholder">
    </div>
  </div>
  <div id="tab2" class="tab">
    <p>Aquí hay una imagen pero esta solapa está oculta</p>
    <img alt="..." src="https://placekitten.com/200/300"/>
  </div>
  <p>Este párrafo no será desplazado.</p>
  ...
  <script>
    const preloadImg = new Image();
    preloadImg.src = 'https://placekitten.com/200/300';
    preloadImg.onload = function() {
      // Puede no ser un div placeholder sino setearlo al contenedor
      document.getElementById('image-placeholder').style.height = this.height + 'px';
    };
    const showTab = function(_tabNumber) {
      document.querySelector('.tab--active').classList.remove('tab--active');
      document.getElementById('tab' + _tabNumber).classList.add('tab--active');
    };
  </script>

1.7 No mostrar el contenido accionable cercano a la imagen hasta que no se cargue

Si no se conoce el tamaño ni la proporción de la imagen, es mejor no mostrar el contenido accionable subsiguiente. El ejemplo más obvio de las interfaces con imágenes de proporción variable es Pinterest. Simplemente no se muestra en pantalla nada interactuable hasta que la imagen que puede desplazarlo no se cargó o se reservó el espacio que ocupará. Para hacer esto se puede utilizar el evento onload del elemento <img> para llamar a la función que haga visibles los elementos accionables vecinos. Ventaja: soluciona el problema Desventaja: el usuario pasa más tiempo esperando. Si le muestran animaciones de carga o skeleton placeholders puede parecerle lento o incluso demodé.
Ejemplo en vanilla js para que se comprenda. Puede reproducirse de manera similar en React, Angular o Vue:

<style>
  .container__actionable-parts {
    display: none;
  }
  // Hasta que no se haya cargado la img y se conozca el alto 
  // no estará listo para ser mostrado y los botones 
  // cercanos permaneceran display: none;
  .container--ready .container__actionable-parts {
    display: block;
  }
</style>
...
<div class="container">
  <img
    alt="..." src="someimage.jpg"
    onload="this.parentElement.classList.add('container--ready')"
  />
  <div class="container__actionable-parts">
    <button>Este botón…</button>
    <p>
      y este párrafo no serán desplazados, porque
      no estarán visibles hasta que la imagen esté
      lista para mostrarse
    </p>
  </div>
</div>

1.8 Permitir que el backend mida la imagen del archivo antes de servirla y comunicar el tamaño al frontend

No importa cual sea el lenguaje del backend seguramente puede leer el tamaño del archivo de la imagen antes de servirla. Por ejemplo Node puede hacerlo con la librería https://www.npmjs.com/package/image-size o similares. Así puede setear en el template (o devolver en el api endpoint como parte del json) cuál es el tamaño original de la imagen para que el frontend calcule el tamaño que ocupará proporcionalmente. Si la url de la imagen se guardará en una base de datos también se pueden guardar como campos integer el ancho y alto de la imagen para que la medición se haga una sola vez. Ventaja: ya que el tamaño en pixeles es un número entero es información que se lee muy rápidamente comparada con el tiempo de carga de una imagen pesada. Desventajas: se debe contar con un backend dinámico; exige mayor uso del servidor y puede demorar un instante más antes de servir la imagen; requiere un endpoint que indique el tamaño y otro que sea la descarga en sí de la imagen; si la imagen no es hosteada en el mismo servidor seguramente requiera descargarla allí temporalmente para medirla; es la solución más compleja.

1.9 Permitir que el usuario que sube la imagen la recorte para adaptarse a la proporción esperada

Ventajas: todas las imágenes quedan del mismo tamaño o proporción y se puede reservar ese espacio con css; si el recorte se hace en el frontend, se sube al servidor una imagen más pequeña así que al mismo tiempo se ahorra tiempo de upload y recursos del servidor.
Desventajas: Requiere compromiso y tiempo del usuario; la interfaz para recortar la imagen puede ser compleja o requerir librerías específicas de manipulación de imágenes.

1.10 Permitir que el backend haga crop y resize automáticamente de la imagen para adaptarla a la proporción esperada

Ventaja: es práctico para el usuario y luego se puede setear por css el mismo espacio para todas las imágenes. Desventajas: consume recursos de proceso y disco del servidor; puede recortar partes de la foto que arruinen lo que se quería mostrar.


En conclusión

En el diseño de UX/UI suele ni siquiera considerarse que los elementos en pantalla tienen tiempos de carga y que el usuario querrá leer los textos o interactuar con los elementos accionables inmediatamente. Muchas veces esto queda a consideración únicamente del programador.

Colegas: valoremos el tiempo y la paciencia del usuario y esforcémonos al máximo por no mover elementos en pantalla mientras el usuario está tratando de interactuar con ellos o leerlos.

Cuando esto se ha planeado y funciona correctamente parece tan natural e inevitable que pasa desapercibido. Querido usuario: si los elementos en pantalla están quietos es porque he puesto atención al detalle.

La quietud de los elementos accionables y de los textos en pantalla es amor ❤️.


Continua pronto en la parte 2 con consideraciones para tipografías, estrategia de render blocking, el contenido dinámico y las notificaciones.


Photo by Ran Berkovich on Unsplash

Posted on by:

patopitaluga profile

Patricio Pitaluga

@patopitaluga

Fullstack dev with 15+ years of xp

Discussion

markdown guide