Optimización en carga de estilos para aplicaciones Rails

Por
Este tutorial se basa en una solución que hemos usado en Get on Board y que grandes sitios han puesto en marcha para mejorar su performance y de paso su indexación en motores de búsqueda. Esta guía utiliza ejemplos pensados para Sprockets, pero es totalmente adaptable para aplicaciones Rails utilizando Webpack.

A través de su herramienta PageSpeed Insights, Google ha difundido algunas técnicas para optimizar la carga de tu sitio. Acá trabajaremos una que está muy relacionada con la manera en que se invocan los estilos.

Es importante aclarar que estos ajustes no harán que tus estilos carguen más rápido, sino que se encargarán de evitar que cuando estos son invocados se produzca un bloqueo en el rendereo del navegador.

¿Pero por qué influye cómo invocamos los estilos en la velocidad de carga?

El CSS es un recurso que es tratado como Render Blocking Resource, lo que implica que el navegador no va a renderizar ningún contenido hasta que haya procesado completamente el CSSOM (CSS Object Model). Acá puedes profundizar un poco sobre esto.

La solución sugerida por Google es dividir la entrega de estilos en dos etapas. Primero debiésemos invocar los estilos básicos para dibujar el contenido principal. A esta primera sección visible del contenido se le llama “above the fold” (ATF de ahora en adelante) y debiésemos invocarla de manera inline dentro de la etiqueta <head>. El resto de los estilos serán llamados de manera asíncrona una vez que el navegador haya terminado de hacer su trabajo para mostrar el contenido.

⚠️ ¿Me sirve esta técnica en mi proyecto Rails?

Si tu aplicación Rails sólo contempla el Back-end y el Front-end está completamente construido con algún framework-librería JavaScript, este artículo NO es para ti.

Si al menos utilizas los layout de las vistas en Rails, este artículo definitivamente ES para ti. No importa si sigues utilizando Sprockets o si ya te pasaste a Webpack, el principio de esta técnica es el mismo.


¿Cómo manejar los estilos inline en una app Rails?

No todas las vistas de tu aplicación necesitarán el mismo esqueleto de estilos, por lo que es recomendable crear dentro del <head> del documento un content_for que le permita a cada vista incluir los estilos que necesite para pintar su ATF.

%head
  = yield(:above_the_fold_styles) if content_for?(:above_the_fold_styles)

Nuestro resultado esperado es que aparezca algo así en nuestro documento HTML:

<head>
  <style>
    .class {font-size: 16px}
  </style>
</head>


Agregar los estilos desde la vista

Para que aparezcan los estilos en el <head> debiésemos hacer algo como esto, ¿No?

- content_for :above_the_fold_styles do
  :css
    .class {font-size: 16px}

Pero hacer esto es muy poco productivo y escalable. ¿Qué pasa si quiero agregar un color que está definido en una variable de SASS? ¿O si quiero utilizar un mixin que calcula mis media queries? Para lograr eso necesitamos pegar acá el contenido de un archivo ya compilado y eso es un poco más complejo. Lo que estamos buscando lograr es algo como esto:

- content_for :above_the_fold_styles do
  = “<style>#{above_the_fold.css}</style>”.html_safe

¿Pero cómo sabe la vista dónde está ese archivo? Para esto existe una manera Rails friendly de lograrlo con Sprockets. La ciencia es la misma que con Webpack, pero probablemente debas chequear tu configuración y confirmar si esta manera de acceder al archivo funciona para ti:

- content_for :above_the_fold_styles do
  = “<style>#{Rails.application.assets['above_the_fold'].to_s}</style>”.html_safe

¡Genial! Pero probablemente no es muy productivo estar escribiendo tanto código en cada vista, por lo que mi sugerencia es crear un helper en el application_helper.rb que sea más o menos así:

def inline_styles_for(file)
  "<style data-turbolinks-track='reload'>#{Rails.application.assets[file].to_s}</style>".html_safe
end

Con esto, en la vista sólo tendrías que hacer lo siguiente:

- content_for :above_the_fold_styles do
  = inline_styles_for(“above_the_fold”)


Llegó el momento de cargar los estilos diferidos

En teoría, los estilos que cargamos inline debiesen darle al usuario la sensación de ya tenerlos cargados, pero la verdad es que sólo tenemos el contenido ATF y debemos estar preparados para que apenas se haga scroll o se ejecute cualquier interacción, el resto de los estilos ya estén cargados en el navegador.

Estos estilos sí serán invocados como un archivo que el navegador descargará desde una etiqueta <link>. Para esto recurriremos exactamente al mismo truco sugerido por Google que utilizando JavaScript le dice al navegador que ignore las etiquetas <link> y que recién las tome en cuenta cuando el navegador le haya mostrado el contenido al usuario.

Lo primero es envolver la invocación de nuestros estilos en una etiqueta <noscript> para que el navegador las ignore. Además le agregaremos un id a esta etiqueta para referenciarlo luego desde el JavaScript:

<noscript id="deferred-styles">
  <link rel="stylesheet" type="text/css" href="deferred_stiles.css"/>
</noscript>

A continuación agregamos el mismo JavaScript de este artículo al final del body para que haga toda la magia. No copies y pegues, intenta entenderlo y ver si cuadra con tus necesidades particulares. A grandes rasgos lo que este script hace es tomar lo que está en la etiqueta <noscript> y meterlo dentro de un <div>, lo que hará que el navegador inicie inmediatamente la descarga de los recursos CSS. Esto se ejecutará en en el siguiente ciclo de pintado del navegador si es que alguna de las variantes de requestAnimationFrame son soportadas. Si no son soportadas, se esperará que se cumpla el evento load del window.

<script>
  var loadDeferredStyles = function() {
    var addStylesNode = document.getElementById("deferred-styles");
    var replacement = document.createElement("div");
    replacement.innerHTML = addStylesNode.textContent;
    document.body.appendChild(replacement)
    addStylesNode.parentElement.removeChild(addStylesNode);
  };
  var raf = window.requestAnimationFrame ||
    window.mozRequestAnimationFrame ||  
    indow.webkitRequestAnimationFrame || 
    window.msRequestAnimationFrame;
  if (raf) raf(function() { window.setTimeout(loadDeferredStyles, 0); });
  else window.addEventListener('load', loadDeferredStyles);
</script>

¿Y si necesito diferentes estilos asíncronos para cada vista?


En la misma línea de lo que hicimos con los estilos inline, crearemos un content_for que nos permita gestionar esto para cada vista. Para eso agregamos el content_for en el <head>:

%noscript#deferred-styles
  = yield(:deferred_styles) if content_for?(:deferred_styles)

Y luego agregamos desde las vistas todas las hojas de estilo que necesitemos

- content_for :deferred_styles do
  = stylesheet_link_tag('deferred_styles')

¡Estamos listos! Ya tenemos nuestra estrategia para cargar estilos inline para pintar el contenido ATF y hemos diferido los estilos no tan relevantes para el primer pintado en nuestra aplicación Rails.


Conclusión

Esto no te garantiza tener todo lo relativo a estilos solucionado. Un problema gigante del que debes hacerte cargo es de la inmensa cantidad de estilos no utilizados que se acumulan en las grandes aplicaciones y que significan una transferencia de datos innecesaria, además de los problemas de mantención de posibles colisiones de estilos.

Ten también en cuenta que es una técnica enredada, que necesita un líder en el equipo que vele porque se sigan estos patrones y porque existan documentaciones actualizadas para futuros miembros del equipo.

Esta misma técnica es la que utiliza AMP y que, si bien es enredada, es muchísimo menos compleja que mantener todo lo necesario de AMP en una aplicación Rails. He pasado por mantener ambas técnicas en grandes sitios y sugiero por lejos probar este tipo de técnicas de manera aislada antes que ir por todo lo que implica adoptar AMP. Tómate el tiempo de analizar tu sitio y seguir las recomendaciones una por una, basándote en lo que será más simple y económico de adoptar para tu equipo y tu proyecto.

Lo más reciente en Blog