Cómo priorizar el contenido visible above-the-fold

Priorizar el contenido visible above-the-fold se refiere a que el estilo que se aplica a renderizar y pintar la parte superior de la página debería estar disponible cuanto antes. Si ese estilo viene en un recurso externo dará lugar a un bloqueo CSS que detiene el proceso de renderizado hasta tener el CSS disponible. La única forma de evitarlo es llevar el estilo afectado a un elemento <style> del <head> del documento. Pero esto es poco eficiente a la hora de mantener el código puesto que cualquier modificación del CSS nos obligaría a recorrer todos los documentos. Una forma de solucionarlo es mediante un pre-procesamiento con PHP para inyectar en cada documento sólamente el estilo que aplica a esa página. En este tema expondré esta técnica que denominaré CSS-FOLD y que se basa en los siguientes principios generales:

  • Todo el estilo será interno, por lo que no usaremos <link rel="stylesheet"> (o reglas @import) pues bloquean el proceso de renderizado.
  • Agruparemos todo el estilo del sitio que afecta a dos o más páginas en un único archivo base.css.
  • Seccionaremos base.css en componentes, que pueden ser componentes de estructura y resto de componentes. Los primeros dan estilo a la estructura de la página como cabecera, barras superiores, inferiores y de navegación. Los segundos dan estilo a funcionalidades específicas como el estilo para un slider de imágenes por ejemplo.
  • Definiremos físicamente que es la parte superior de la página.
  • Los componentes que aparezcan en esa parte y que además sean necesarios cuando la página se carga irán al estilo que denominaremos Before. El resto irán al estilo After, esto es, los de la parte inferior de la página y los no necesarios de la parte superior en la carga.
  • Algunos componentes pueden separarse en Before y After de forma fija y permanente porque siempre se comportarán así, como el estilo que estructura la página. El resto serán de uno u otro estilo dependiendo de donde aparezcan y como se comporten en cada página.
  • Usaremos PHP para extraer los componentes Before y los inyectaremos en un elemento <style> del <head> del documento. Los componentes After también irán en un <style>, pero los ubicaremos dentro de un <noscript> al final de la página.
  • Así el navegador tendrá el estilo de la parte superior disponible para no bloquear la carga. El estilo de la parte inferior será inyectado con JavaScript tras el evento load y cuando el usuario interaccione por primera vez con la página. Usaremos los eventos click y scroll para ese propósito.

Todo el estilo del sitio en un único archivo CSS

Todo el estilo de este sitio que afecta a dos o más páginas se encuentra en base.css. Está seccionado en componentes:

ComponentesNombreAplicaFinalidad
Estructurageneral-before, general-afterTodas las páginasEstructura general de la página
nav-before, nav-afterTodos las páginas de temasListas de navegación por apartados y "migas"
xhtml-cssGlosario XHTML-CSSEstructura específica para ese glosario
Otrosaplicacion, cubo-color, desplegable, ejemplo, ...No todas las páginasEstilo para módulos y funcionalidades específicas

En otros componentes sólo he relacionado los cuatro primeros para abreviar, pero hay unos 14 más. Aquí hay una muestra del estilo de los componentes general-before y general-after

/*<general-before>*/
    /* CABECERA -------------------------------------------------------*/
    #cabeza-menu {
        position: absolute;
        top: 0;
        ...
        }
    ...
    /* MENÚ -------------------------------------------------------------*/
    #menuwx {
        color: rgb(49, 99, 98);
        font-family: Arial, Helvetica, sans-serif;
        ...
        }
    ...
    .menuwx-desp {
        display: none;
        ...
        }
    ...
/*</general-before>*/
/*<general-after>*/
    /* INTERIOR DEL MENÚ DE CABECERA------------------------------------*/
    .menuwx-desp > ul {
        margin: 0.4em 0.4em -0.2em 0.4em;
        padding: 0.2em;
        ...
        }
    ...
/*</general-after>*/
...

He quitado cosas para aclararnos. Se trata de marcar un componente con una etiqueta dentro de un comentario. En el código tenemos /*<general-before>*/ y se cierra como hacemos con las de HTML /*</general-before>*/. Los mismos comentarios no deben aparecer en otras partes del archivo y además no serán eliminados cuando lo minifiquemos, pues estas marcas servirán para filtrar con PHP los componentes que vayamos a inyectar. En este bloque tenemos por ejemplo el estilo para la cabecera de la página, un elemento con id="cabeza-menu". Se compone del logotipo, el título Wextensible y una barra con botones Inicio, Menú y Pie. El botón de menú despliega una lista de enlaces de cabecera que se encuentra en un elemento con class="menuwx-desp". Dado que inicialmente está oculto, el estilo de la lista <ul> que contiene los enlaces podemos pasarla con el estilo de la parte inferior en el bloque /*<general-after>*/. Cuando el usuario interaccione con la página haciendo click en el botón de menú, el elemento class="menuwx-desp" se pondrá con display = "block" al mismo tiempo que se cargará con JavaScript el bloque general-after que da estilo a la lista de enlaces.

En definitiva, se trata de separar estilo Before incluyéndolo en el bloque general-before. El estilo After que aparece en la parte inferior o bien que sólo aparece después de que el usuario interaccione (con un click, por ejemplo), lo pondremos en el bloque general-after. Esto en cuanto al estilo de estructura que aparece en todas las páginas. Luego tendremos otros bloques de estructura como nav-before y nav-after que aplica sólo a aquellas páginas que tienen una lista de navegación por apartados y otra a modo de "migas". Y finalmente otros componentes, como por ejemplo cubo-color que sólo será necesario cuando usemos el módulo de JavaScript cubo-color.js. Será un estilo Before o After dependiendo de su uso concreto en una página, en función donde estará ubicado el HTML afectado por ese estilo y si es necesario que ese estilo esté disponible cuando la página se carga.

Definiendo la parte superior de la página

Podemos definir como parte superior de la página (o above-the-fold) al contenido que aparece al cargar la página en el monitor de 19 pulgadas de un ordenador de sobremesa (desktop) con zoom 100% y con la ventana totalmente expandida. Los contenidos que estén por debajo serán accesibles cuando el usuario use el scroll. Esta delimitación de la parte superior es excesiva para dispositivos de menor tamaño, pero nos aseguramos que cubrimos la mayor parte de los dispositivos.

La separación del estilo Before y After habrá que llevarla a cabo cuando estemos desarrollando la página. Observaremos donde aparece un componente cuando visualicemos la página en el navegador definiéndolo entonces en uno u otro lado.

Cabe preguntarse que pasa si se usa un monitor de mayor altura. En ese caso el estilo no estaría disponible para algún componente que con esa pantalla queda en la parte superior mientras que con un monitor de 19" quedaba en la inferior. Quizás una posible mejora de esta técnica podría consistir en obtener las medidas de la pantalla con JavaScript y en caso de ser mayor de 19" forzar el inyectado del estilo inferior sin esperar a que el usuario interaccione.

Con el tema del zoom pasa algo parecido. Si el usuario lo tiene ajustado en un valor menor del 100% puede suceder que contenidos que aparecían en la parte inferior ahora lo hagan en la superior. En móviles esto no sucede porque utilizo el meta viewport con content="width=device-width, initial-scale=1", forzando el zoom inicial al valor 100%. Pero esto no funciona en ordenadores de sobremesa. Esto y lo anterior son mejoras que habría que contemplar en un futuro.

Esquema de una página con CSS before y CSS after

Una página básica llegará al navegador de esta forma:

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8" />
    ...
    <style id="css-before">
        ...ESTILO BEFORE (inyectado con PHP)...
    </style>
    ...
</head>
<body>
    ...
    <script>
        var wxG;
        window.onload = function () {
            wxG = Wextensible.general;
            wxG.adjudicarEventosGenerales();
        };
    </script>
    <script src="/res/inc/general.js" async></script>
    <noscript id="nojs">
        <style id="css-after">
            ...ESTILO AFTER (inyectado con PHP)...
        </style>
    </noscript>
    ...
</body>
</html>

El estilo Before se inyectará con PHP dentro de un elemento <style id="css-before"> y el navegador lo tendrá disponible en la cabecera del documento para mejorar la velocidad de carga. El estilo After se incluirá dentro de un elemento <style id="css-after"> pero a su vez dento de un <noscript>. Esto significa que el navegador considerará todo lo que hay dentro como simple texto siempre que JavaScript no esté desactivado. Dicho de otra forma, si JavaScript está activado el navegador no considerará ese contenido como estilo y por tanto no lo renderizará con la carga de la página. Posteriormente recuperaremos ese texto con JavaScript y lo inyectaremos directamente en el <head> de la página.

En la última remodelación de este sitio he procurado no perjudicar excesivamente la presentación de los contenidos cuando JavaScript está desactivado. Metiendo el estilo After dentro de un <noscript> conseguimos también ese propósito. Si JavaScript está desactivado el navegador renderizá el estilo After de forma directa.

CSS FOLD: Priorizar CSS above-the-fold con PHP

Desde que apliqué la compresión web a este sitio todos los documentos HTML son tratados como documentos PHP. Para ese cometido uso los scripts PHP de pre-ejecución. En ese tema se explica como hacerlo para comprimir la página con PHP, pero también nos puede servir para el objetivo de este tema, que es hacer uso de PHP para inyectar los estilos Before y After. Más arriba vimos como llega una página al navegador. El siguiente código es tal como está escrita la página actual, antes de ser tratada por PHP:

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8" />
    ...
    <?php
        echo extraerCss("/res/sty/base.css", array(
        	"before" => array("general-before", "nav-before", "tabla", "resaltador"),
           	"after" => array("general-after", "nav-after")
        ));
    ?>
    <style id="css-before"><?php echo $css_fold["before"]; ?></style>
    ...
</head>
<body>
    ...
    <script>
        var wxG;
        window.onload = function () {
            wxG = Wextensible.general;
            wxG.adjudicarEventosGenerales();
        };
    </script>
    <script src="/res/inc/general.js" async></script>
    <noscript id="nojs"><style id="css-after"><?php echo $css_fold["after"]; ?></style></noscript>
</body>
</html>

El script PHP de pre-ejecución prehjc.php es un cargador de páginas para enviar cabeceras HTTP de tipo de contenido y relacionadas con el cacheado a raíz de usar compresión PHP. Pero también contiene la función extraerCss($href, $arr) que se encarga de inyectar el estilo Before y After. Tiene dos argumentos. El primero es una ruta al archivo CSS que contiene todo el estilo con marcas de componentes como vimos más arriba. El segundo argumento es un array con dos posiciones: before con una lista de componentes que se insertarán en el primer <style> y la otra posición es after para el que está al final de la página. La función extrae esos componentes y los almacena en la variable global $css_fold. Desde ahí luego los inyectamos en los elementos <style>.

El código anterior es el de la página actual. Vemos que los componentes de estructura general-before y nav-before se inyectan en la parte superior, mientras que general-after y nav-after en la inferior. Estos son los componentes de la estructura de la página y del navegador de apartados con el título de la página que aparece por debajo de la cabecera. En Before ponemos también el estilo para tablas y para el resaltador, puesto que esos componentes aparecen inicialmente en la parte superior de la página. De esta forma cuando elaboramos una página simplemente tendremos que agregar componentes en estilo Before o After según las particularidades de cada página. Veámos la función extraerCss() quitando lo relativo a una traza de ejecución y otros detalles no importantes para explicar el código:

<?php
$css_fold = array("before"=>"", "after"=>"");
function extraerCss($href, $arr){
    global $css_fold;
    if (($contenido = @file_get_contents($_SERVER["DOCUMENT_ROOT"].$href)) !== false){
        foreach ($arr as $s=>$componentes){
            $extraido = "";
            for ($i=0,$maxi=count($componentes); $i<$maxi; $i++){
                $pos1 = strpos($contenido, "/"."*<".$componentes[$i].">*"."/");
                $pos2 = strpos($contenido, "/*"."</".$componentes[$i].">*"."/");
                $len = strlen($componentes[$i]);
                if (($pos1===false)||($pos2===false)){
                    //...aquí hay un tratamiento de errores para la traza
                } else {
                    $extraido .= substr($contenido, $pos1+$len+6, $pos2-$pos1-$len-6);
                }
            }
            $css_fold[$s] = $extraido;
        }
        return "<!-- CSS FOLD: correcto -->";
    } else {
        return "<!-- CSS FOLD: error -->";
    }
}
?>

El archivo CSS se abre con file_get_contents() pues esa función tiene la particularidad de que su contenido es cacheado en memoria en el servidor, tal como explico en un apartado sobre los tiempos de lectura de los archivos de índices para un buscador interno. El filtrado de componentes se realiza detectando las marcas con la función strpos() que es mucho más eficiente que usar expresiones regulares. Cada componente lo vamos almacenando en el array $css_fold, variable global que tendremos disponible luego en el HTML/PHP para inyectarlo en los elementos <style>. Gracias al comportamiento de la función file_get_contents() se consiguen tiempos de proceso insignificantes. En el script original incluyo también algo de código para una traza. Si observa el código fuente de una página, verá algo como esto:

    
<!-- CSS FOLD: tiempo 0.32ms, before 8.5KB, after 4.1KB (previo a gzip) -->    
    

Es un comentario inyectado con la función original indicando el tiempo de proceso y los tamaños de los componentes Before y After. Son tamaños previos a la compresión de la página, hecho que se producirá después de que PHP la haya tratado y antes de ser enviada al navegador. Esos datos son de la página de inicio de este sitio y desde el servidor en producción. El tiempo es despreciable. Y de los 30 KB que contiene el archivo base.css con todo el estilo del sitio, hemos filtrado 12.6 KB que se utilizan en la página de inicio. Y de estos sólo 8.5 KB forman el CSS crítico para la parte superior de la página. Los otros 4.1 KB se inyectarán con JavaScript como explicaremos a continuación. El HTML de la página de inicio ocupa unos 19 KB. Inyectando estos 12.6 KB del CSS se eleva a 31.6 KB. Pero tras comprimirla llega al navegador con un tamaño de 9.3 KB. A pesar del añadido CSS sigue siendo de un tamaño relativamente pequeño lo que supone una ventaja a la hora de la carga.

CSS FOLD: Inyectar CSS below-the-fold con JavaScript

El estilo para la parte inferior de la página está ahora dentro de un elemento noscript. Como decía antes, si JavaScript está activado el contenido de ese elemento es puro texto plano. Por lo tanto podemos usar JavaScript para inyectarlo en la cabecera del documento. Esto lo hacemos desde el window.onload dentro de la función adjudicarEventosGenerales().

    ...
    <script>
        var wxG;
        window.onload = function () {
            wxG = Wextensible.general;
            wxG.adjudicarEventosGenerales();
        };
    </script>
    <script src="/res/inc/general.js" async></script>
    <noscript id="nojs"><style id="css-after"><?php echo $css_fold["after"]; ?></style></noscript>
</body>
</html>

Con la última remodelación del sitio he eliminado todos los eventos en línea, es decir, los que ponemos en un atributos como onclick en el elemento. Ahora espero a la carga de la página y ahí hago la adjudicación de eventos como por ejemplo, los que accionan los botones de la barra superior de la página. Como esa función la tengo en todas las páginas del sitio me servirá para incluir el script necesario para cargar el CSS After. El código es el siguiente, donde he omitido esos eventos generales para centrarnos en la carga del CSS After:

adjudicarEventosGenerales: function(omitirCssAfter) {
    window.setTimeout(function(){
        wxG.adjudicarEventos(...);
        //... OTRAS ADJUDICACIONES DE EVENTOS...
        //Carga de Css After
        if (!omitirCssAfter) {
            //Si la ruta contiene # entonces tenemos que cargar el css-after inmediatamente
            //porque podrá llevar a la parte inferior de la página donde puede haber un
            //componente CSS que pintar. En otro caso declaramos eventos para cargar el
            //css-after cuando hagamos click o usemos scroll
            if (window.location.href.indexOf("#")>-1){
                wxG.cargarCssAfter();
            } else {
                wxG.removerListener = true;
                if(document.body.addEventListener){
                    document.body.addEventListener("click", wxG.cargarCssAfter, false);
                } else if (document.body.attachEvent){
                    document.body.attachEvent("onclick", wxG.cargarCssAfter);
                }
                if(window.addEventListener){
                    window.addEventListener("scroll", wxG.cargarCssAfter, false);
                } else if (window.attachEvent){
                    window.attachEvent("onscroll", wxG.cargarCssAfter);
                }
                //Ponemos el scroll a cero para que se vea sólo la parte superior de la página.
                //Chrome no aplica scroll a documentElement y Firefox no lo aplica a body.
                document.documentElement.scrollTop = 0;
                document.body.scrollTop = 0;
            }
        }
    }, 1);
},

Se trata de declarar un evento click y otro scroll para cuando el usuario los active por primera vez se ejecute cargarCssAfter(). Tenemos en cuenta algunos detalles, como cuando la ruta trae un posicionamiento interno con # de tal forma que el navegador aplicará el scroll para reubicar la página a la altura de ese ancla. En ese caso no aplicamos eventos y cargamos directamente el CSS After. En otro caso tras aplicar los eventos tenemos que reubicar el scroll a cero, puesto que si se recarga una misma página con un scroll distinto de cero, los navegadores mantienen ese valor de scroll. Pero necesitamos que la página se muestre inicialmente en su parte superior por lo que siempre el scroll tendrá que estar a cero. Veámos la función que carga el CSS After:

/* Carga de CSS After, estilo que se carga a modo de 'below de fold'
 */
removerListener: false,
cssAfterCargado: false,

cargarCssAfter: function(){
    var sty = document.getElementById("nojs");
    if (sty && !wxG.cssAfterCargado){
        var css = wxG.getInnerText(sty);
        if (css == ""){
            //IE7-8 no pueden leer el texto dentro del noscript. Pedimos el recurso
            //completo aunque suponga un exceso de carga
            var linke = document.createElement("link");
            linke.rel = "stylesheet";
            linke.href = "/res/sty/base.css";
            document.getElementsByTagName("head")[0].appendChild(linke);
        } else {
            document.getElementsByTagName("head")[0].innerHTML += css;
        }
        if (wxG.removerListener){
            if (document.body.removeEventListener){
                document.body.removeEventListener("click", wxG.cargarCssAfter, false);
                window.removeEventListener("scroll", wxG.cargarCssAfter, false);
            } else if (document.body.detachEvent) {
                document.body.detachEvent("onclick", wxG.cargarCssAfter);
                window.detachEvent("onscroll", wxG.cargarCssAfter);
            }
        }
        //Esto es por si falla wxG.removerListener no se vuelva a ejecutar esta carga
        wxG.cssAfterCargado = true;
    }
}

Con la variable cssAfterCargado controlamos que la función se ejecute una única vez. Desde el elemento <noscript> recuperamos el texto con la función getInnerText() que tengo incluida en el archivo general.js y que no es más que recuperar con textContent para los navegadores modernos. Lo inyectamos directamente en el <head> con innerHTML. Esto funciona con los navegadores que he podido consultar, pero no con IE7-8, porque parece que no almacena el contenido de un <noscript> si JavaScript está activado. Como solución extrema se inyecta un elemento <link rel="stylesheet"> para traer el recurso CSS base.css completo.

Como esta función de carga del CSS After sólo se ejecuta una vez, ya podemos eliminar los eventos que adjudicamos en un paso anterior. Tanto en la adjudicación como ahora aplicamos una diferenciación para IE antiguos que usan attachEvent y dettachEvent en lugar de addEventListener y removeEventListener.