Dejar las cosas quietas: parte 1
Tags: html, javascript, webdev
Published: 2020-08-06 14:22:32

> 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:**{% codepen https://codepen.io/pato_pitaluga/pen/NWNWGZO %}

--------

## ¿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](https://dev-to-uploads.s3.amazonaws.com/i/wwtsd9x3z7wcbks78ich.gif)
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 **&lt;img&gt;** 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 **&lt;img&gt;** 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**.
```html
<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 **&lt;style&gt;** deberá estar antes o inmediámente después del elemento **&lt;img&gt;** ya que si se carga asincrónicamente pueden ocurrir unos segundos mientras el browser renderea el alto del elemento de su estilo.

```html
<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 &lt;img&gt; 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](https://es.wikipedia.org/wiki/Regla_de_tres) 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:
```html
<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á**:
```css
.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 **&lt;img&gt;** 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 &lt;img&gt; 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:**
```html
  <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 **&lt;img&gt;** 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:**
```html
<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](https://unsplash.com/@berko?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)  on [Unsplash](https://unsplash.com/s/photos/carousel?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)