Conoce cómo funciona el browser y optimiza tu código frontend

26/nov/2014 12

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 <head /> 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 <link />. 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 <b> dentro de <p> 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.

Quitémosle peso innecesario al browser

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 <link> estén dentro del <head> 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 <head> mediante <style></style>) 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 <link>.

<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 <script></script> 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.

Links:

Comentarios

  1. nikoskip [#]

    Lo de basket.js está muy bueno, no lo conocía, gracias por compartir.

    Pienso que hay que tener ojo cuando se trabaja en sitios mobile respecto a cargar cosas ‘async’. En redes móviles la latencia es tremenda, de acuerdo a lo que he leído e investigado es mucho mejor intentar concatenar y minifcar nuestros assets, a cargarlos de manera asíncrona… bueno, siempre depende del caso. Al respecto, es muy interesante esta charla: https://www.youtube.com/watch?v=7gtf47D_bu0

    Saludos.

    • Jaime [#]

      Muy buen post Jorge.

      Sólo agregar que también se debe tener cuenta el rendimiento del sitio una vez ya cargado y renderizado.
      ¿como es eso? el proceso de renderizado se divide en fases como lo explicas en el articulo, estás fases son
      layout, painting y composite. El renderizado no se hace sólo una vez, se hace cada vez que algún elemento dentro del árbol DOM cambie su posición o forma, como? modificando sus propiedades CSS.
      Hay propiedades CSS que gatillan estas 3 fases, otras 2 y otras ninguna. Algunas desencadenan las 3 fases al cambiar su valor por defecto, pero sólo 2 o ninguna cuando actualizamos su valor.

      Por ejemplo, la propiedad height gatilla layout, paint y composite. Tanto cuando cambiamos su valor por defecto, como cuando actualizamos un valor previamente establecido por uno mismo. Height hace cambiar la geometría del elemento, esto puede hacer cambiar la posición y dimensiones de otros elementos en la pantalla por ende el browser debe hacer operaciones de layout nuevamente.
      Al recalcular estructura y dimensiones (layout) , se deben volver a realizar operaciones de pintado (painting) (porque cambiaron dimensiones) esto hace que también se desencadene composite. composite básicamente es juntar todas las layers y texturas pintadas por separado, dibujarlas en el orden que corresponda y convertirlas en una sola “imágen”: la página renderizada.

      es muy común realizar muchos cambios de propiedades a elementos cuando el puntero se pasa por encima de un elemento (:hover),
      este es uno de los principales puntos por donde se degrada la experiencia de uso. Aplicar muchos cambios CSS para crear determinados efectos, exige trabajo constante al browser re calculando estructura, re pintando y re componiendo la página, haciendo que veamos el sitio “lento” por ejemplo al hacer scroll, al observar que ciertas animaciones o transformaciones no se ven como esperamos o no “se cortan”, etc etc.

      es un laaargo tema pero dejo un link donde hay una lista con algunas propiedades CSS y que tan “caras” en coste de rendimiento son cada una:
      http://csstriggers.com/

      Saludos y ojala tenga segunda parte este post 😀

      • Jaime [#]

        me auto corrijo en el primer parrafo, redacté mal:
        “”Hay propiedades CSS que gatillan estas 3 fases, otras 2 y otras ninguna””
        por
        “”Hay propiedades CSS que gatillan estas 3 fases, otras 2 y otras solo una””

      • Jorge Epuñan [#]

        Gracias por tu excelente aporte Jaime, en este artículo quise enfocarme en la performance desde el server hacia el browser; lo q mencionas se enfoca del browser hacia el usuario, lo q da para otro artículo.

        Abrazos.

  2. HTML5 - Artysmedia [#]

    […] Conoce cómo funciona el browser y optimiza tu código frontend […]

  3. Creando con HTML5 - Artysmedia [#]

    […] Conoce cómo funciona el browser y optimiza tu código frontend […]

  4. Ezgi [#]

    Good job, thanks for sharing.

  5. ¿Es posible conseguir un 100/100 en Google Pagespeed? [#]

    […] Un buen artículo: Conoce cómo funciona el browser y optimiza tu código frontend | CSSLab […]

  6. >: ( [#]

    “Aplicar muchos cambios CSS para crear determinados efectos, exige trabajo constante al browser re calculando estructura, re pintando y re componiendo la página, haciendo que veamos el sitio “lento” por ejemplo al hacer scroll, al observar que ciertas animaciones o transformaciones no se ven como esperamos o no “se cortan”, etc etc.”

    Que es lo que le pasa a este sitio web. Muy bonito, muchos estilos, pero se ha abusado tanto de ellos que va lento a mas no poder (la página principal es horrorosa de lo mal que va) y carga muy muy lenta. Carga tan lento que pensaba que no había contenido.

    • Jorge Epuñan [#]

      ¯\_(ツ)_/¯ no se puede agradar a todos, muchos halagos y este es la primera queja. Enfin, gracias por tus palabras.

      • Amado [#]

        No creo que el comentario se haya hecho con mala intención, la web es muy bonita y atractiva sin embargo si va algo lenta con los estilos y animaciones, aún así tu artículo es muy bueno y me ayudo a comprender varias cosas. Te mando un saludo, felicitaciones y gracias.

  7. Altacom ¿Es posible conseguir un 100/100 en Google Pagespeed? [#]

    […] Un buen artículo: Conoce cómo funciona el browser y optimiza tu código frontend | CSSLab […]

Deja tu Comentario

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

CSSLab