Optimización de CSS

Ramón Saquete

Escrito por Ramón Saquete

Índice de contenidos

En esta guía de optimización de CSS (Cascading Stylesheets), vamos a estudiar qué es necesario para que la descarga y el pintado de una página web se hagan de forma óptima.

En la guía de optimización de JavaScript ya se explicó cómo ha crecido en importancia el WPO en el cliente, permitiéndonos conseguir usuarios más fieles y mejor posicionamiento en móvil.

Desde que los usuarios usan más el móvil 📱 para navegar ha crecido la importancia de WPO, permitiéndonos conseguir mejor posicionamiento y usuarios más fieles. Clic para tuitear

Recordemos que en la guía de JavaScript, comentaba que lo óptimo, según el modelo de rendimiento RAIL de Google es:

  • Respuesta de la interfaz en menos de 100ms.
  • Animaciones con dibujado completo cada 16ms que son 60 FPS (imágenes por segundo).
  • Inhabilitado: cuando el usuario no interactua con la página, lo que se ejecuta en segundo plano, no debe durar más de 50ms.
  • Load: la página debe cargar en 1000ms.

CSS afecta en el modelo RAIL a las animaciones y a la carga de la página. Recordemos también que, para cargar la página, el navegador sigue la ruta de representación crítica: descarga del HTML >> construcción del DOM >> descarga de CSS y JS >> construcción del CCSOM >> construcción del árbol de representación >> layout o reflow >> pintado >> composición.
Las técnicas de optimización que vamos a ver a continuación, van a afectar a todas estas etapas, por lo que si las llevamos a cabo correctamente el tiempo de carga total puede mejorar sustancialmente.

Optimizar descarga e interpretación

Para este punto es importante comprender la cascada de descargas (Network waterfall de las herramientas para desarrolladores). Cuando se descarga una página, unos archivos llaman a otros y se crea una estructura de dependencias en árbol por niveles que serían los siguientes:

Cuando se descarga una página, unos archivos llaman a otros y se crea una estructura de dependencias en árbol por niveles. Clic para tuitear
  1. En el primer nivel tendríamos el HTML, que es lo primero en descargar y constituiría el tronco del árbol.
  2. En el segundo nivel (las ramas), tendríamos los recursos de imágenes, CSS y JavaScript enlazados desde el HTML.
  3. En el tercer nivel (las hojas), tenemos las imágenes, fuentes y CSS adicionales enlazados desde los CSS descargados en el paso anterior. El JavaScript, una vez que se está ejecutando, también puede solicitar archivos adicionales de cualquier tipo.
  4. Podríamos tener un cuarto nivel (que podrían ser los nervios de las hojas), ya que es posible tener un CSS que descargue otro que a su vez descargue otro más y así podríamos tener una gran cantidad de niveles que harían la descarga totalmente ineficiente.

Ahora que ya sabemos como dependen unos archivos de otros vamos a ver de qué forma se crea la cascada de descargas:
El navegador usa un algoritmo llamado escáner de precarga para determinar que archivos solicitados en el HTML bloquean la ruta de representación crítica y así solicitar estos recursos críticos lo antes posible. Igualmente tiene un escáner de precarga para el CSS. Estos algoritmos apenas entienden el HTML o el CSS, sólo buscan los archivos enlazados para pedirlos lo antes posible.
Los recursos críticos de cada nivel se intentan bajar en paralelo, esto quiere decir, que esos archivos se piden y se descargan a la vez. Pero el paso de un nivel a otro ocurre secuencialmente (los archivos de un nivel se piden a continuación del anterior), ya que es necesario que el escáner de precarga analice los archivos para saber qué otros archivos hay que descargar.
Puesto que la petición de archivos de forma secuencial es mucho más lenta que lanzar peticiones en paralelo (es más rápido pedir la información cuanto antes), la mayoría de las técnicas que expongo a continuación están orientadas a evitar nuevos niveles del árbol de dependencias (evitar las peticiones de forma secuencial), o al menos que exista el menor número de archivos críticos posibles en los niveles superiores, ya que éstos serán los últimos archivos en descargarse.

La petición de archivos de forma secuencial es mucho más lenta que lanzar peticiones en paralelo, ya que es más rápido pedir la información cuanto antes. Clic para tuitear

En cuanto a la descarga, creo necesario explicar que en HTTP1.1 la descarga en paralelo es posible hasta un número determinado de archivos dependiendo del navegador, mientras que con HTTP2 no hay límite en el número de archivos que se descargan en paralelo, ya que en realidad, internamente es una descarga secuencial multiplexada sobre el mismo flujo de datos, en otras palabras, con HTTP2 es como si bajáramos un sólo archivo más grande que ocupará los mismos recursos de red que varios archivos en paralelo, pero con menos información y sin el coste de enviar las peticiones por separado y de forma secuencial por bloques.
A continuación vemos un ejemplo visual de cómo se produce la cascada de descarga:

Cascada de descargas de una web
Cascada de descargas de las herramientas para desarrolladores de Google Chrome

En definitiva, para una descarga rápida queremos árboles de dependencias que tengan el menor número posible de ramas y hojas que sean recursos críticos (que no son necesarios para pintar la página inicialmente).

Para una descarga rápida queremos árboles de dependencias que tengan el menor número posible de ramas y hojas que sean recursos críticos. Clic para tuitear

Unificar, minimizar y comprimir desde un preprocesador

Siempre es recomendable no utilizar CSS directamente, si no utilizar un lenguaje por encima como Sass, Less o Stylus. Estos lenguajes te permiten hacer el código más limpio y mantenible, ya que tienen una sintaxis que permite escribir menos, se pueden definir variables, reutilizar código, separar el código en archivos y además, en el caso de Sass, usar estructuras de control de programación.
Un archivo escrito en alguno de estos lenguajes hay que pasarlo por un preprocesador que genere el CSS unificado y minimizado con su source map asociado. Estos son archivos .map que nos servirán para analizar el CSS en Chrome como si estuviéramos trabajando con los archivos originales. Este trabajo se puede hacer antes de subir el archivo a la web con alguna herramienta como webpack, gulp, grunt, etc. o, directamente, con las herramientas de los propios lenguajes. También hay librerías para hacerlo desde la propia programación de la web, pero esta opción no es recomendable porque es un proceso costoso. Pero, si se hace, siempre se debe cachear el resultado para no generar el CSS con cada visita.
Al igual que con JavaScript, recomiendo cargar en cada página dos CSS, uno global para toda la web y otro específico para la página. De esa manera no hacemos descargar al usuario estilos que no son necesarios para la página que está viendo.

Se recomienda cargar en cada página dos CSS, uno global para toda la web y otro específico para la página. para no hacer descargar al usuario estilos que no son necesarios para la página que está viendo. Clic para tuitear

Cachear en el navegador con las cabeceras de caché del protocolo HTTP

Usaremos la cabecera Cache-control con los valores public y max-age, con un tiempo de caché grande, de al menos 1 año. Cuando queramos invalidar la caché cambiaremos el nombre del archivo. Este cambio de nombre puede hacerlo automáticamente el framework que se use para desarrollar y la herramienta para transpilar o preprocesar el CSS.

Comprimir con brotli q11 y gzip

Para los archivos CSS podemos seguir las mismas recomendaciones que di en el artículo sobre optimización de JavaScript y en el de compresión gzip.

Eliminar selectores no utilizados

CSS trabaja con selectores que son patrones formados principalmente por elementos, clases, identificadores, pseudo-clases y pseudo-elementos que sirven para seleccionar aquellas partes del DOM de la página a las que queremos dar estilo mediante propiedades como «color», a las que podemos asignar distintos valores. Por ejemplo:
Sí tenemos una regla simple como la siguiente:

#pagina .producto a{  /* <- selector{ */ 
     color:blue;      /* <- declaración formada por: propiedad:valor; */ 
}

Estamos diciendo que en una página con una estructura como la siguiente, los enlaces serán azules:

<main id="pagina">
   <article class="producto">
       <a href="/url.html">nombre del producto</a>
   </article>
</main>

Si no tenemos ninguna estructura en la página con la que encaje la regla, ese código no será necesario. Con la herramienta UnCSS, podemos incluir esta técnica en el proceso de generación del CSS desde Sass si usamos gulp, grunt o broccoli.

Aprovechar la herencia y los estilos en cascada para crear menos reglas

Algunas propiedades como color, line-height, text-indent, text-transform, font y sus variantes, … al aplicarlas a un elemento del HTML, todos los elementos contenidos en éste heredan el valor de esta propiedad. Así que, por ejemplo, podemos crear la siguiente regla:
body{
font-family:sans-serif;
}
De esta forma, estamos estableciendo el tipo de fuente para todo el documento, sin que sea necesario especificar el tipo de fuente para cada elemento de forma individual.
CSS es el acrónimo inglés de hojas de estilo en cascada y en cascada significa que los estilos se aplican en un orden en el que las reglas más prioritarias sobrescriben a otras. ¿De qué forma podemos usar esto para escribir menos? Por ejemplo, en un blog como este, podemos tener reglas para dar un estilo a todas las entradas de un listado, pero luego podemos sobreescribir algunas propiedades de éste para dar estilo a las entradas destacadas, sin tener que duplicar parte del que ya hemos creado. Para saber hacer esto el desarrollador debe conocer las normas de especificidad de los selectores de CSS ya que, a parte de la ubicación de los estilos y el orden de las reglas, es lo que determina qué reglas tienen prioridad sobre otras. Por ejemplo, supongamos que tenemos un CSS con las siguientes reglas en este orden:

/* Regla 1 */ 
.clase2 div.clase1 div {
  color:green;
}

/* Regla 2 */ 
div {
  color:blue!important;
}

/* Regla 3 */ 
div.clase1 div:first-child {
  color:yellow;
}

/* Regla 4 */ 
#id1 .clase1 div {
  color:red;
}

Y este sería el fragmento de HTML al que queremos dar estilo:

<div id="id1" class="clase2">
    <div class="clase1">
        <div>Hola</div>
    </div>
</div>
CSS es el acrónimo inglés de hojas de estilo en cascada, y en cascada significa que los estilos se aplican en un orden en el que las reglas más prioritarias sobrescriben a otras. Clic para tuitear

Todas las reglas anteriores afectan al color de la palabra «Hola», un desarrollador que sepa las normas de especificidad de los selectores, debería ver sin pensar que las reglas se aplican en el siguiente orden, de la más prioritaria a la menos prioritaria: Regla 2, Regla 4, Regla 1, Regla 3. Por lo tanto, en este caso la palabra «Hola», se verá de color azul por la regla 2, aunque ésta se ha hecho utilizando la directiva «important», algo que un buen desarrollador no haría salvo en determinados casos.

Agrupar las reglas

Si tienes que dar el mismo estilo con varios selectores distintos, agrupa los selectores separándolos por comas. Ejemplo:
En lugar de escribir:

.clase1{
    color:red;
}

.clase2{
    color:red;
}

.clase3{
   color:red;
}

Esto se puede escribir de forma más abreviada como:

.clase1,.clase2,.clase3{
    color:red;
}

Usar las declaraciones abreviadas

Podemos abreviar varias declaraciones, como las del siguiente ejemplo:

padding-top: 5px;
padding-right: 5px;
padding-bottom: 5px;
padding-left: 5px;

Para ello se debe utilizar su correspondiente declaración abrevicada:

padding: 5px;

De esta manera escribimos menos y el CSS tendrá menos código que descargar.

Utilizar CSS sprites de forma inteligente

El término sprite en informática surge inicialmente del mundo de los videojuegos para referirse a los cuadrados que contienen imágenes y personajes (personajes como duendecillos, que es la traducción literal de sprite). En CSS el término sprite hace referencia a rectángulos de imágenes que se van a mostrar en la web y que están agrupados en una imagen más grande. Al juntar varias imágenes en una, evitamos tener que descargar varios archivos y así no limitamos la capacidad de descarga en paralelo del navegador si usamos HTTP1.1. Con HTTP2 esta técnica no aporta ninguna mejora.
Procurar juntar las imágenes que, por sus características, se adapten mejor a la compresión de un tipo de formato de imagen. De esa manera podemos tener algunos sprites en JPG, otros en PNG-8, otros en PNG-24, etc.
Usar esta técnica correctamente no consiste en cargar 300 iconos en una imagen para luego sólo usar uno. Esto es algo muy habitual y no es la forma correcta, porque estamos cargando y procesando una imagen grande para luego mostrar sólo una pequeña parte.

CSS sprites de la página inicial de Google
CSS sprites de la página inicial de Google (la imagen está rotada y reducida para que la puedas visualizar sin hacer scroll)

Empotrar imágenes con DATA-URIs

El esquema URI de datos o DATA-URI, nos permite empotrar imágenes con cadenas codificadas de texto dentro del CSS. Esto hace que las imágenes ocupen más, ya que un archivo en formato binario ocupa menos que en texto. Pongamos un ejemplo rápido, si en un archivo tenemos el valor binario 1111 1111 que en decimal es el número 255, en el texto tendremos que tener los caracteres 2 – 5 – 5 que en binario se codifican en ASCII con todos estos ceros y unos: 0011 0010 – 0011 0101 – 0011 0101. Estos son más ceros y unos de los que teníamos originalmente, por lo que el archivo ocupará más. Este aumento de espacio prácticamente se elimina por completo si estamos comprimiendo el CSS con brotli o gzip, por lo que no es un problema. Así que a pesar de eso, está es una técnica de optimización muy buena, porque estaremos ganando bastante en tiempo de carga, al evitar bajar un nivel más en el árbol para descargar las imágenes.
Si usamos esta técnica no es necesario usar CSS sprites, pero si nos gusta organizar las imágenes en sprites, podemos combinar las dos técnicas.
Hay que tener en cuenta que si una misma imagen se usa en varias partes del CSS, tendremos que reorganizar la maquetación para que sólo aparezca una vez, asignando la misma clase a los elementos en el HTML que van a mostrar la imagen. Si no lo hacemos así, estaremos descargando varias veces la misma imagen.
También hay que prestar atención a las imágenes que no se usan o que sólo se usan en una parte de la web, porque al contrario de lo que ocurre con las imágenes enlazadas normalmente, vamos a descargar siempre esas imágenes, las usemos o no. Así que esta técnica se debe aplicar sólo a imágenes que sean recursos críticos. Si no habéis seguido la recomendación de tener dos CSS por cada página (uno con los estilos globales y otro para los de la página actual), llevar cuidado con esto.
Las imágenes que empotremos es mejor que estén al final del CSS y no deben ser muy grandes, ya que el CSS es un recurso crítico que bloquea la representación de la página y no puede tardar demasiado en descargarse.
Por último, podemos usar esta técnica para empotrar imágenes directamente en el HTML dentro del atributo «src» del elemento «img», pero en este caso tampoco recomiendo usar está técnica con imágenes grandes, ya que si en el HTML tenemos muchos bytes que no pertenecen al contenido podemos tener problemas de indexación. También se debe tener en cuenta que, cuando la imagen se repite en varios sitios o varias páginas, es mejor no empotrarla en el HTML para que se pueda cachear y que así no se descargue varias veces.

Adelantar la carga de imágenes y fuentes desde el HTML

Esta técnica es preferible a usar DATA-URIs, porque las imágenes se pueden cachear en el navegador de forma independiente al CSS y sin aumentar el tamaño del mismo. La podemos aplicar añadiendo a la cabecera etiquetas como esta:

<link rel="preload" href="bg-image-narrow.png" as="image" media="(max-width: 600px)" />

Si adelantamos la petición de imágenes enlazadas desde el CSS, no habrá que esperar a que se descargue el CSS y que lo analice el escaner de precarga. Esto se debe aplicar sólo a recursos críticos, ya que no queremos precargar recursos innecesarios.
Con esta etiqueta se puede indicar que archivo vamos a precargar y de que tipo es, incluso podemos especificar una media query para que se cargue sólo con determinados tamaños de pantalla.
No confundirlo con rel=»prefetch» que es para precargar recursos para la siguiente página que se vaya a visualizar.

No usar @import

La regla import de CSS (no confundirlo con el import de Sass o Less), permite importar archivos CSS desde dentro de otros. Si habéis prestado atención ya sabréis que esto es algo muy malo para el rendimiento, porque estamos aumentando los niveles del árbol de dependencias, haciendo que un CSS se descargue otro que a su vez puede descargar otro más o descargar imágenes.

Usar media=»print» para los estilos de impresion

En un hoja de estilo podemos establecer cómo se imprimirá la web al mandarla a la impresora. Esto se puede hacer dentro del CSS global con una media query o sacarlo en un hoja de estilo independiente, de esta manera:

<head>
   ...
   <link rel="stylesheet" type="text/css" href="/css/main.css">
   <link rel="stylesheet" type="text/css" href="/css/print.css" media="print">
   ...
</head>

En el código de arriba el archivo main.css bloquea la representación de la página, por lo que se manda a descargar inmediatamente por el escáner de precarga. Sin embargo, el archivo print.css no lo bloquea y se descarga con baja prioridad. Así que de esta manera no bloqueamos el pintado con la descarga de los estilos de impresión.
Podemos usar está técnica también con las media queries que se utilizan para hacer la web responsive, pero si eso va afectar negativamente a la forma de organizar nuestro código, no lo recomiendo.

Solicitar los estilos en la cabecera

Las hojas de estilo deben solicitarse con elementos link desde la cabecera, porque a parte de ser su ubicación correcta, son recursos críticos que se deben bajar cuanto antes para pintar la página.

Empotrar el CSS en la página sólo si es muy poco

En las páginas hechas con AMP se obliga a empotrar en la página el código CSS y limitarlo a un máximo de 50.000 bytes. Esta técnica, también se puede aplicar en páginas que no sean AMP y se hace de la siguiente forma:

<style> /* <style amp-custom> si es AMP */ 
/* código CSS */ 
body{
    background:#0AF;
}
</style>

En el ejemplo he puesto comentarios, pero el código se debe empotrar totalmente minimizado y sin comentarios.

Esta técnica hace que no tengamos que pedir el archivo CSS (evita bajar al segundo nivel en el árbol de dependencias), pero si el archivo CSS es muy grande, Google no llegará a indexar los bytes del contenido de la página, así que por eso, sólo debemos hacer esto con archivos pequeños. Lo ideal es implementarlo de forma que no entorpezca el flujo de trabajo normal y que no repitamos el mismo código a lo largo de varias páginas, así que podemos usar esta técnica para cargar el CSS que es específico de la página que estamos visitando y cargar el CSS global con la etiqueta link.

No introducir estilos en línea

Si utilizamos el atributo style en cada elemento de la página para darle estilo, estaremos mezclando el estilo con el contenido y duplicando los estilos que usamos a lo largo de varias páginas y elementos. Por eso usar este atributo se considera un anti-patrón de rendimiento y mantenibilidad.

Si utilizamos el atributo style en cada elemento de la página para darle estilo, estaremos mezclando el estilo con el contenido y duplicando los estilos que usamos a lo largo de varias páginas y elementos Clic para tuitear

Si es un caso en el que sabemos que el estilo no se va a repetir, podemos hacer una excepción a esta regla. Por ejemplo, tenemos que poner una imagen decorativa de fondo que sólo se usa en un sitio y lo hacemos así:
<div style=»background:url(‘/img/imagen.jpg’);»></div>
Esto hará que la imagen se pueda descargar antes de tener que pedir el CSS y no estaremos repitiendo código. Aunque si no queremos mezclar estilo y contenido, se puede dejar más limpio llevando el estilo a la cabecera dentro de una etiqueta <style> y poniéndole un identificador a la capa para poder referenciarla.

Organizar bien el CSS para evitar hacer más reglas de las necesarias

Hay muchas formas de organizar el código CSS de forma limpia y elegante. Hay muchas metodologías: ITCSS, BEM, SMACSS, OOCSS, SUITCSS, Atomic, … pero independientemente de cual se utilice, lo importante es que si se estructura bien el código se repetirá menos y eso además de repercutir en la mantenibilidad, también lo hará en el rendimiento.
Si a la hora de la verdad aparece algún caso donde el código CSS no encaja con ninguna de las recomendaciones del método usado, recomiendo inventar nuevas normas propias para que no se pierda la organización.

Evitar HTML complejo

No hay que asignar estilos a base de crear nuevos elementos en el HTML en lugar de identificadores y clases, ni tampoco crear elementos HTML que no sean necesarios ni para dar estilo, ni para aportar información semántica.
Esto es porque si tenemos menos nodos del DOM, éste se cargará antes, ocupará menos memoria y, en consecuencia, el CSSOM será menos complejo. Además, al usar identificadores y clases, tendremos selectores de CSS más eficientes.

Optimización de fuentes

Las fuentes son un recurso crítico que bloquea el pintado de los textos en la página. Mientras se descarga la fuente, algunos navegadores mostrarán cómo queda la página con una fuente del sistema y otros la mostrarán sin texto. Al terminar la descarga, el usuario ve como la fuente cambia o aparece, lo cual no genera buena impresión.
Este es un tema que requiere una entrada aparte pero a grandes rasgos diré que no hay que cargar más fuentes de las necesarias, ya sean variantes de una misma fuente (distintos grosores de negrita, cursiva, negrita y cursiva, …) o juegos de caracteres que no vamos a usar en el idioma de la página actual. Usar la compresión Gzip o Brotli sólo con los formatos antiguos EOT y TTF (no con WOFF o WOFF2 porque ya están comprimidos) y si quieres controlar como se carga la fuente, usa la Font Loading API de JavaScript.
También podemos precargarlas desde el HTML así:

<link rel="preload" href="fonts/cicle_fina-webfont.woff" as="font" type="font/woff" crossorigin="anonymous">

En este artículo podrá consultar más en profundidad cómo optimizar las fuentes de un sitio Web.

Optimizar el pintado

Evitar CSS expressions

Las CSS expressions permiten calcular valores de propiedades utilizando JavaScript. Esto, además de violar la separación de comportamiento y estilo, es muy ineficiente para el navegador. Así que es mejor reemplazar estas expresiones por JavaScript.

Crear animaciones con CSS antes que con JavaScript

Con CSS animations se pueden hacer animaciones creando interpolaciones entre los valores de las propiedades a lo largo de una serie de fotogramas clave, al igual que se hacía con Flash. Para el que no sepa a que me refiero, interpolar es la operación matemática de calcular los valores intermedios entre varios valores dados. En este caso permite calcular los valores de las propiedades de CSS a lo largo de los puntos clave establecidos para que el navegador pueda realizar la animación. Idealmente se debe usar sólo la propiedad transform que nos permite girar, mover, agrandar o estirar cualquier capa y la propiedad opacity, para hacer transparencias.
Ademas de usar sólo las propiedades transform y opacity, recomiendo hacerlo con capas que tengan posición fija o absoluta (para evitar que la animación desplace el resto de capas), porque de esta manera se evita que el navegador lance el reflow y el pintado, y sólo será necesario que ejecute la composición de las capas.
Ejemplo:

<style>
#animated_div_wrap{
    position: relative;
    height: 70px;
}

#animated_div {
    position: absolute;
    width: 70px;
    height: 47px;
    box-sizing: border-box;
    background: #ba1c1e;
    color: #ffffff;
    font-weight: bold;
    font-size: 20px;
    padding: 10px;
    animation: animated_div 5s infinite;
    border-radius: 5px;
}

@keyframes animated_div
{
    0% {
        transform: rotate(0deg);
    }
    25% {
        transform: rotate(-5deg);
    }
    50% {
        transform: translateX(320px) rotate(-20deg);
    }
    100% {
        transform: translateX(0px) rotate(-360deg);
    }
}
</style>

<div id="animated_div_wrap">
       <div id="animated_div">WPO</div>
</div>

Resultado:

WPO


Hacer la misma animación con JavaScript sería mucho menos eficiente porque se ejecutaría en el hilo de JavaScript junto a muchas otras cosas y el navegador está optimizado para ejecutar las animaciones de CSS de forma fluida, priorizando la velocidad sobre la suavidad. Esto último quiere decir que si una animación consiste en desplazar una capa 1000 píxeles a la derecha en un segundo, en el que debemos mostrar 60 fotogramas, la capa se desplazará 16 píxeles en cada fotograma. Pero si al navegador sólo le da tiempo a pintar la mitad de fotogramas, la capa se desplazará 32 píxeles en cada repintado.

Evitar reflow y repintado especificando las proporciones de la imagen con una capa por encima

Cuando se pinta una página, puede que algunas imágenes se carguen al final, haciendo que se tenga que desplazar todo lo que hay debajo de éstas cuando ya se han cargado. Para evitar estos saltos, cada imagen debe estar contenida en una capa que ocupe el mismo alto que ocupará la imagen cuando termine de cargar. Esto hay que implementarlo teniendo en cuenta que la imagen tendrá distintos tamaños dependiendo del dispositivo y se hace como indico a continuación:
Primero se calcula la proporción del alto de la imagen respecto al ancho:
Proporción alto = alto/ancho * 100
Esto nos da un porcentaje que se lo pondremos a la propiedad «padding-top» de una capa anterior a la imagen, de esa manera esta capa ocupará el alto de la imagen independientemente de que se modifique el ancho. La imagen tendrá posición absoluta respecto a una capa contenedora para que se monte encima de la capa que establece el alto. Veamos un ejemplo:

<style>
#wrap-img-ej{
   position:relative;
   max-width:800px;
}

#wrap-img-e div{
   padding-top:56.25%;
}

#wrap-img-e img{
   position:absolute;
   top:0;
}
<style>
<div id="wrap-img-ej">
   <div></div>
   <img src="/img/imagen.jpg" alt="texto SEO" />
</div>

Esta técnica también se debe aplicar a sliders y carruseles, donde es frecuente que al ejecutarse JavaScript se provoque un salto en la carga inicial.
Para hacer el uso de esta técnica más ágil, podemos crear una plantilla del código de ejemplo anterior con un web component o usar directamente el web component amp-img de la biblioteca de componentes de AMP. El código equivalente al del ejemplo anterior con el componente de AMP, sería el siguiente:

<amp-img alt="texto SEO" src="/img/imagen.jpg" width="800" height="450" layout="responsive"></amp-img>

Si en el selector existe un identificador no hay que complicarlo innecesariamente

Pongamos que tenemos una regla con un selector como el siguiente:

div .clase1 #mi-id.clase2{ 
    background:red; 
}

Con este HTML:

<div>
    <div class="clase1">
        <div id="mi-id" class="clase2">
            <div class="clase3">WPO</div>
            <div>CSS</div>
        </div>
    </div>
</div>

Este selector no tiene mucho sentido porque en realidad sólo puede existir un identificador «mi-id» en la página, así que el siguiente selector funcionará exactamente igual y tiene menos código, por lo que tarda menos en cargar y ejecutarse:

#mi-id{ 
    background:red; 
}

Con esto no quiero decir que cuando exista un identificador en la regla haya que quitar todo los demás sistemáticamente. El siguiente selector sí que tiene sentido:

#mi-id .clase3{ 
    background:blue; 
}

Usar translateZ para que el navegador utilice la aceleración gráfica de la GPU

En las capas con animaciones o simplemente con transparencia (propiedad opacity), utiliza will-change:transform; o translateZ(0), para promover la capa al hardware de la GPU donde se pintará más rápido. Pero evita usar en exceso estas reglas de promoción a la GPU, ya que las capas requieren memoria y administración por parte del navegador.
will-change: transform;
transform: translateZ(0);
Las dos propiedades tendrán el mismo efecto, pero «translateZ(0);», con la que decimos que haga un desplazamiento en el eje Z a la posición inicial, funciona en todos los navegadores, mientras que will-change, es la forma correcta de implementarlo porque no modifica propiedades de la capa.

Agrupar varias transformaciones en una matriz de rotación

Las transformaciones de un objecto vectorial se implementan multiplicando el vector de cada vértice del objeto por una matriz que nos da la posición final de cada vértice. Esta matriz define el tipo de transformación que vamos a aplicar (posición, escala, rotación, …). Esto es lo que ocurre internamente en el navegador cuando a una capa de CSS le aplicamos una transformación bidimensional o tridimensional con la propiedad transform.
Si multiplicamos todas las matrices que generan las transformaciones que vamos a hacer, ya sea de posición, escala, rotación o deformación, podemos aplicarlas todas a la vez en un solo paso con la matriz resultante, escribiendo mucho menos código y de forma computacionalmente más óptima, ya que usaremos la función matrix de esta forma: transform:matrix(a11, a21, a12, a22, a13, a14) en lugar de algo como transform:transform(x,y) skew(x,y) rotate(x,y) scale(x,y). Si es una transformación 3D usaremos matrix3D en lugar de matrix. Además esta función nos permite hacer la transformación de efecto espejo que no se puede hacer de otra manera:

WPO CSS

Para el cálculo rápido de la matriz de transformación (no es fácil hacerlo de cabeza), será necesario usar alguna librería, programa o función de Sass para calcularlo rápidamente, teniendo en cuenta siempre que las transformaciones, al igual que la multiplicación de matrices, no es conmutativa (trasladar y girar no es lo mismo que girar y trasladar).

Matrices de transformación 2D
Matrices de transformación 2D

Simplificar la complejidad de los efectos que son difíciles de representar

La propiedad filter es computacionalmente costosa de aplicar cuando la usamos para hacer difuminados, cambios de contraste, de brillo, etc. No obstante, puede ser un buen aliado para evitar cargar más imágenes, si queremos aplicar alguno de estos efectos al pasar el ratón por encima de alguna.
Otros efectos costosos de pintar, son las sombras y los degradados. Si tienes estos efectos y detectas que la etapa de pintado final de la página tarda mucho, prueba a quitarlos.

Utilizar CSS antes que imágenes

Por lo general siempre va a ser más rápido pintar algo con CSS que utilizar una imagen equivalente, ya que la imagen tiene un tiempo de descarga, de lectura, descompresión y pintado que suele superar al de descarga y pintado de CSS. Por eso, si no puedes simplificar el diseño, como indico en el punto anterior, no debes sustituir, por ejemplo, una sombra de un texto con CSS por su imagen equivalente.

No maquetar con tablas

Esto parece una recomendación de la prehistoria de la web, pero sigue siendo cierta y más aún si queremos un buen rendimiento.
Las tablas son ineficientes porque un cambio en el tamaño de una celda afecta a la posición de todos los elementos que hay por debajo y, si cambia en ancho, puede que también a celdas superiores.

Maquetar con grid layout y flexbox

Utilizar Grid Layout y/o flexbox es más óptimo y flexible que maquetar con capas flotantes y, por su puesto, que con tablas. Aquí podemos hacer uso de un framework como Bootstrap, que en la versión 4 ya usa flexboxes.
Con esta nueva forma de maquetar tienes la ventaja de poder cambiar visualmente la ubicación de cualquier elemento, independientemente del orden en el que se esté pintando en el HTML, con mayor libertad que con capas flotantes. Esto nos deja tener una distribución totalmente distinta en móvil y escritorio y, dejar más arriba del HTML, lo primero que queramos que se indexe por cuestiones de SEO.

Evitar selectores extremadamente complejos

Normalmente, en las herramientas automáticas se sugiere evitar selectores complejos, pero yo no recomiendo hacer mucho caso a esta regla porque siempre es más importante la mantenibilidad del código que el rendimiento. Así que mi recomendación es evitar sólo los selectores extremadamente complejos, aunque es muy raro que se tenga que hacer algo tan ineficiente como por ejemplo esto:

div p a *{}

Para saber cuando un selector es complejo o ineficiente debemos de tener en cuenta que las partes del selector más generales (los elementos) son menos eficientes que las más específicas (identificadores). También tenemos que entender que los selectores se interpretan de derecha a izquierda, así que tener las partes más generales del selector más a la derecha hace la regla más ineficiente, porque esto hace que el navegador tenga que examinar más partes del DOM para saber si tiene que aplicar la regla. En el ejemplo de arriba el operador *, selecciona todos los elementos de la página, después mira si todos los antecesores de todos los elementos de la página tienen un enlace, luego mira si todos esos enlaces tienen un elemento p antecesor y así sucesivamente, a no ser que en algún punto no se encuentre ningún elemento, en cuyo caso se detiene la comprobación del selector.
Los pseudo elementos y pseudo clases de CSS3 también son lentos (:first-child, :nth-child(), ::firts-line, ::first-letter).

Herramientas

Utiliza las herramientas para desarrrolladores de Chrome y Firefox para analizar los tiempos de carga y de pintado, ajustando las pruebas para ver qué ocurre en dispositivos con una potencia de procesamiento más limitada y con una red más lenta. En las herramientas para desarrolladores de Google Chrome, estas últimas opciones aparecen pinchando en el engranage rojo de la derecha de la pestaña «Performance». Aquí, además, tenemos la opción «Enable advanced paint intrumentation» que nos permite ver la pestaña «Paint Profiler» al pinchar en un evento de pintado. De esta manera podemos ver todo lo que necesita hacer el navegador para pintar nuestra página:

examinador de perfil de pintado de las herramientas para desarrolladores de Google Chrome
En las herramientas para desarrolladores de Google Chrome y en la pestaña rendimiento, podemos examinar lo que ocurre en el pintado en el «Paint Profiler».

 

Para estudiar la velocidad del CSS y también como le afecta el JavaScript, son muy útiles las opciones de la pestaña representación (rendering), que nos muestran, entre otras cosas, en tiempo real, qué rectángulos se repintan en la página y la velocidad en FPS a la que se pinta. Esta opción la podemos sacar al pinchar en la misma pantalla que vemos en la captura anterior, en los tres puntos de arriba a la derecha y pulsando en «Show console drawer» y pinchando en la pestaña «Rendering»:

Opciones de representación de devtools de Chrome para ver en tiempo real como afecta el mover el scroll o las animaciones.
Opciones de la pestaña «rendering» de las herramientas para desarrolladores de Chrome para ver en tiempo real como afecta mover el scroll o las animaciones.

Recomendaciones finales

Es importante priorizar siempre la mantenibilidad sobre el rendimiento, por eso recomiendo escribir siempre el código de la manera más limpia posible, usando alguna metodología de organización, dando nombres relevantes a las clases e identificadores y, si cometes del error de no usar un preprocesador, al menos pon todos los tabuladores, saltos de línea y espacios, porque después ya se podrá minimizar el código con una herramienta.

Ten en cuenta siempre la ley de Amdhal  que dice así:

La mejora obtenida en el rendimiento de un sistema debido a la alteración de uno de sus componentes está limitada por la fracción de tiempo que se utiliza dicho componente

Esto quiere decir que siempre se debe priozar la optimización de la parte que consume más tiempo, porque será lo que más impacto tenga en el rendimiento global.

Ramón Saquete
Autor: Ramón Saquete
Desarrollador web y consultor SEO técnico en la agencia de marketing online Human Level, es experto en WPO e indexabilidad.

Únete a la conversación

3 comentarios

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

  1. Acabo de leer el artículo, la verdad es que soy iniciada en esto, y me gustaría que me aclarases o me dieras varias razones «fundamentales» para separar el contenido html y css en una web, por ejemplo en una página de producto de un ecommerce.
    Muchas gracias

    1. Hola Ángel,
      La razón principal es la mantenibilidad del código, ya que si escribes el CSS en cada HTML cuando quieras cambiar un estilo que aparece en varias páginas, tendrás que modificar todos los HTML, con la pérdida de tiempo que supone buscar donde modificarlo en cada una, mientas que si lo tienes separado en una hoja de estilo externo, sólo lo tendrás que modificarlo una vez para toda la Web. Así te aseguras que estos estilos son los mismos para toda la web y no te has dejado ninguno sin cambiar o lo has modificado de forma diferente al resto.
      De forma más secundaria, también se puede hacer por rendimiento, si los usuarios suelen visitar varias páginas cuando visitan nuestro sitio o tenemos muchos usuarios recurrentes, estos tendrán que cargar en cada página que visiten los estilos que comparten dichas páginas, mientras que si están en una hoja de estilo externa, sus estilos se cachearán en el navegador en la primera visita y en la siguiente página que visiten, sea la misma o no, ya tendrán esos estilos cargados.
      Podemos incrustar el CSS en el HTML, excepcionalmente cuando tenemos estilos que son sólo de la página visitada o cuando tenemos una página con estilos distintos al resto de la web, para adelantar la descarga del CSS y que se pinte antes. Pero si haces esto, procura que todos los estilos estén dentro de una etiqueta <style> en la cabecera y no mezclado con el HTML con atributos style, ya que también puedes tener estilos que se repitan varias veces dentro de una misma página.
      La clave para tener código mantenible, es no repetir código, lo que se llama principio DRY (Don’t Repeat Yourself), y tenerlo organizado de forma que sea fácil de encontrar algo a la hora de hacer un cambio. Ninguna de estas dos cosas se puede lograr si los estilos están mezclados con el HTML.
      Saludos