Hace no más de 2 años que la tendencia de los usuarios de internet han visto cómo han crecido de manera indirecta las siguientes afirmaciones:

  • Los sitios han crecido en peso de sus archivos (CSS, JS, imágenes, tipografías).
    • La cantidad de usuarios que utilizan redes móviles (2G, 3G y 4G) ha crecido mucho, pero muuuucho.

Por ende:

Hay poca consciencia de que sitios pesados y redes móviles limitadas no son buenos coeficientes de una misma ecuación.

Esto debe llevarnos a una nueva manera de pensar sobre la optimización de sitios relativos al front-end. Y no sólo por los usuarios y las experiencias que puedan llevar un lento desempeño de tus sitios en sus dispositivos; recuerda que Google considera dentro de su algoritmo de posicionamiento que el tiempo de renderizado sea menor a 1 segundo, suponiendo que con 3 segundos el usuario ya se fue de tu sitio y esperando 10 segundos lo más probable es que nunca más vuelva.

En este artículo mostraré cómo funciona un browser desde que inicia el render del código de una página y con esa información algunas técnicas para aumentar la optimización de los elementos que la componen.

Tramo crítico de renderizado

Por años se consideró como velocidad de carga como la rapidez en que vemos la estructura, diseño y contenido de la página cargada. Pero luego Google definió el tramo crítico de renderizado (critical rendering path) como la secuencia de pasos que el browser toma de convertir código y recursos asociados en la vista inicial de una página web. Esto cambia la noción completamente, orientándola hacia lo esencial, lo fundamental en tu esa página para después cargar lo secundario. ¿El foco? Contenido más rápido, mayor pagerank.

A continuación mostraré los procesos que suceden cuando ingresas una URL en el browser y el servidor resuelve lo que esa URL contiene:

De nada a contenido

Básicamente lo primero que entregará un servidor será código HTML como el siguiente:

<!DOCTYPE html>
<html lang="es">
  <head>
    <title>Título</title>
    <link href="style.css" rel="stylesheet">
  </head>
  <body>
      <p>Texto <b>en negrita</b>.
      <img src="imagen.jpg">
    </p>
  </body>
</html>

Lo que hace el navegador es parsear este código dentro del DOM (Document Object Model), el que corresponde a la representación en forma de árbol del lenguaje HTML. El browser construye el DOM incrementalmente, o sea, comienza a parsear el HTML apenas el primer trozo de código es recibido y agrega nodos a la estructura del árbol tanto como sea necesario:

img1

En este punto la ventana del navegador aún no muestra nada, pero se está referenciando a un archivo style.css dentro de Nótese que los estilos de un sitio son parte crítica del renderizado dado que tienen que ser descargados apenas el parser HTML pase por . Si tenemos definido como estilos básicos:

p { font-weight: normal; }
p b { display: none; }

Éstos son parseados dentro de CSSOM ó CSS Object Model, el que desafortunadamente no puede ser construido incrementalmente como el DOM debido al efecto cascada que determina la naturaleza de CSS; imagina que después de la declaración CSS anterior definieras:

p { font-weight: bold; }

Lo que causaría que sobre-escribieras la primera declaración de p {}, demostrando que se debe esperar que todo el CSS sea descargado y procesado antes podamos renderizarlo. Por ende, CSS bloquea el render hasta que la representación de CSS y HTML sean entendidos por el browser, construyendo recién en ese momento el árbol de renderizado (render tree). Esta estructura combina el DOM y CSSOM pero considerando sólo los elementos visibles:

img2

Como habrán notado, el dentro de

no está considerado debido a que no es visible para el DOM (display: none;) mediante CSS.

Los pixeles recién aparecen luego de 2 pasos: estructura (layout) y pintura (paint). La estructura se encarga de calcular las posiciones y dimensiones de cada elemento respecto al viewport actual; ya la pintura agrega los colores, formas, sombras y demás efectos de estilo terminando de mostrar la página renderizada. Cada vez que el árbol de render cambia (por ej. mediante JavaScript) o el viewport cambia (utilizando interfaces líquidas, adaptativas ó responsive) estructura y pintado vuelven a crearse.

Estructuras líquidos/responsive tienen mayor carga en el layout/paint que estructuras adaptativas

El tramo crítico de renderizado completo se ve en el siguiente diagrama:

img3

¿Y las imágenes?

No son considerados críticas para la construcción del DOM, por lo tanto, no bloquean el renderizado de una página. Pero sí influyen y bloquean el evento Load, el que corresponde a la carga y proceso de todos los elementos considerados dentro de un HTML adicionando CSS y JavaScript. Así vemos que las imágenes deben ser siempre optimizadas pero no bloquean el tramo crítico de renderizado.

No olvidemos a JavaScript

JavaScript es otro actor que influye directamente en nuestro tramo crítico de renderizado. A partir del primer código HTML entregado, lo expandiremos agregando JavaScript:

<!DOCTYPE html>
<html lang="es">
  <head>
    <title>Título</title>
    <link href="style.css" rel="stylesheet">
  </head>
  <body>
      <p>Texto <b>en negrita</b>.
      <script>
      document.write('<h1>olakease</h1>');
      var elem = document.querySelector('h1');
      elem.style.color = 'green';
	    </script>  
      <img src="imagen.jpg">
    </p>
  </body>
</html>

Este sencillo script demuestra que cambiar el DOM puede tambien influir en el CSSOM. Mientras JavaScript agrega un nuevo elemento al DOM <h1>olakease</h1>, el parser debe parar hasta que el script sea ejecutado por completo. Luego se determina el color del elemento recién creado, lo que hace que CSSOM esté presente antes de que el script sea ejecutado.

Ahora aislemos el script llevándolo a un llamado de un archivo externo .js**:**

<!DOCTYPE html>
<html lang="es">
  <head>
    <title>Título</title>
    <link href="style.css" rel="stylesheet">
  </head>
  <body>
      <p>Texto <b>en negrita</b>.
      <script src="app.js"></script>  
      <img src="imagen.jpg">
    </p>
  </body>
</html>
img4

El nuevo archivo externo realiza un request adicional, pero a pesar del lugar en que lo llames dentro del HTML, siempre CSS será parseado primero. Apenas CSSOM es interpretado, el contenido del script puede ser ejecutado y solo después de esto el parser del DOM puede terminar de ser ejecutado. Es un juego de parser-render-bloqueo-render-bloqueo-parser.

Técnicas de optimización

Ahora que conocemos los conceptos, es hora de anotar cómo podemos poner algo en práctica. Hay 3 puntos claves donde puedes optimizar el tramo crítico de renderizado y que permite que el browser produzca resultados visibles con mayor rapidez.

No olvides tener en cuenta que debes utilizar estos consejos con cautela y sabiduría; hazlo con cuidado, asesórate y recuerda que no existen recetas milagrosas ni fórmulas de éxito; cada situación es particular.

Minimiza los bytes que son llamados por el servidor

Regla simple: mientras más liviano tu sitio, más rápido carga y renderiza. Sencillo. Minifica, comprime y utiliza caché en tus assets estáticos y tu HTML. No temas comprimir el código HTML removiendo espacios blancos (whitespaces) y comentarios innecesarios cuando vayas a ambiente productivo.

Minimiza el CSS que bloquea el renderizado

Recuerda que CSS bloquea al renderizado y ejecución de JavaScript, así que entregar los estilos al usuario lo más rápido posible es imperativo. Asegúrate que todas las etiquetas estén dentro del de tu documento HTML para que el navegador las cargue y renderice de inmediato.

Otra estrategia es disminuir el CSS que bloquea el renderizado mediante media queries. Digamos que nuestro sitio de ejemplo tiene estilos para impresión y reglas declaradas para dispositivos móviles en orientación landscape. Puedes separar el CSS en varios archivos y dejar que el browser las parsee condicionalmente:

<head>
  <title>Sitio Ejemplo</title>
  <link href="style.css" rel="stylesheet">
  <link href="print.css" rel="stylesheet" media="print">
  <link href="landscape.css" rel="stylesheet" media="orientation:landscape">
</head>

Claramente el peso de nuestro style.css será menor debido al código removido y que repartimos entre los otros 2 archivos .css, los cuales serán utilizados sólo cuando realmente necesarios. Esto no quiere decir que no serán cargados; el navegador los descarga al inicio pero en menor prioridad que el principal y en paralelo al proceso de renderizado.

Otro recurso es agregar como estilo inline (dentro del mediante ) evitando que el browser realice un request al servidor por un nuevo archivo. Esta técnica se utiliza para el primer render, el que se realiza above de fold. El CSS que comanda lo que primero está en el viewport del browser, para luego continuar con el resto de la estructura mediante archivos enlazados con .

<head>
  <title>Sitio Ejemplo</title>
  <script>
  header { ... }
  header nav { ... }
  header .logo { ... }
  </script>
  <link href="style.css" rel="stylesheet">
</head>

Algunas herramientas que te ayudan a calcular el CSS crítico (above the fold) y separar los estilos para servir esta vista son:

Finalmente, ¿que tal llamar tus archivos .css de forma asíncrona? Suena descabellado pero se creó loadCSS, una función JavaScript que carga CSS de forma asíncrona, especial para esos pesados @font-face por ejemplo.

Minimiza el bloqueo del parser de JavaScript

Lo misma regla ocurre con JavaScript. Si necesitas unas pocas líneas de código en el render inicial, considera agregarlos dentro de tu HTML mediante ahorrarás descargas al servidor y por ende tendrás una respuesta más rápida al usuario.

En un caso óptimo llamas todo tu JavaScript en un mismo archivo al final de tu documento HTML. Pero en otros pasos, escribiendo código modular, puedes separar tus archivos y llamarlos asincrónicamente siempre y cuando no interactúen con el DOM ni el CSSOM:

...
    <script src="retardo.js" async></script>
  </body>
</html>

Con esto le dices al navegador que no necesita ejecutar el script en el momento en que es llamado en documento HTML. Esto también permite que el browser continúe construyendo el DOM y ejecute scripts cuando el DOM esté completo. Imagina código de Analytics, redes sociales en este retardo.js, el que no interactúa con el DOM ni el CSSOM.

Finalmente, una técnica y herramienta interesante es basket.js donde en teoría, cargas tus script críticos y recurrentes y los guardas en el localstorage del browser (HTML5), para utilizarlo desde ahí mientras dure la navegación del usuario y cuando éste regrese.

Suena bien. Manos a la obra.