Cómo cargar fuentes de una manera que combata FOUT y haga feliz a Lighthouse
El flujo de trabajo de una fuente web es simple, ¿verdad? Elija algunas fuentes atractivas listas para la web, obtenga el fragmento de código HTML o CSS, colóquelo en el proyecto y verifique si se muestra correctamente. La gente hace esto con Google Fonts un millón de veces al día, colocando su link
etiqueta en el archivo head
.
Veamos qué tiene que decir Lighthouse sobre este flujo de trabajo.
Hemos hecho todo según el libro, la documentación y los estándares HTML, entonces, ¿por qué Lighthouse nos dice que todo está mal?
Hablemos sobre la eliminación de las hojas de estilo de fuentes como recurso que bloquea la representación y analizamos una configuración óptima que no solo haga feliz a Lighthouse, sino que también supere el temido destello de texto sin estilo (FOUT) que generalmente viene con la carga. de fuentes. Haremos todo eso con HTML básico, CSS y JavaScript, para que pueda aplicarse a cualquier pila tecnológica. Como beneficio adicional, también veremos una implementación de Gatsby, así como un complemento que desarrolló como una solución sencilla para ello.
Qué queremos decir con fuentes que “bloquean el renderizado”
Cuando el navegador carga un sitio web, crea un árbol de representación desde DOM , es decir, un modelo de objetos para HTML, y CSSOM , es decir, un mapa de todos los selectores de CSS. Un árbol de renderizado es parte de una ruta de renderizado crítica que representa los pasos que sigue el navegador para renderizar una página. Para que el navegador muestre una página, necesita cargar y analizar el documento HTML y cada archivo CSS vinculado en ese HTML.
Aquí hay una hoja de estilo de fuente bastante típica extraída directamente de Google Fonts:
@font-face { font-family: 'Merriweather'; src: local('Merriweather'), url(https://fonts.gstatic.com/...) format('woff2');}
Quizás esté pensando que las hojas de estilo de fuentes son pequeñas en términos de tamaño de archivo porque normalmente contienen, como máximo, unas pocas @font-face
definiciones. No deberían tener ningún efecto notable en el renderizado, ¿verdad?
Digamos que estamos cargando un archivo de fuente CSS desde un CDN externo. Cuando se carga nuestro sitio web, el navegador debe esperar a que ese archivo se cargue desde la CDN y se incluya en el árbol de renderizado. No solo eso, sino que también debe esperar a que se solicite y cargue el archivo de fuente al que se hace referencia como valor de URL en la definición de CSS.@font-face
En pocas palabras: el archivo de fuente se convierte en parte de la ruta de procesamiento crítico y aumenta el retraso en el procesamiento de la página.
¿Cuál es la parte más vital de cualquier sitio web para el usuario medio? Es el contenido, por supuesto. Es por eso que el contenido debe mostrarse al usuario lo antes posible en el proceso de carga de un sitio web. Para lograrlo, la ruta de renderizado crítico debe reducirse a recursos críticos (por ejemplo, HTML y CSS críticos), con todo lo demás cargado después de que se haya renderizado la página, incluidas las fuentes.
Si un usuario navega por un sitio web no optimizado con una conexión lenta y poco confiable, se molestará si se queda sentado en una pantalla en blanco esperando que terminen de cargar los archivos de fuentes y otros recursos críticos. ¿El resultado? A menos que ese usuario sea muy paciente, lo más probable es que simplemente se dé por vencido y cierre la ventana , pensando que la página no se carga en absoluto.
Sin embargo, si se posponen los recursos no críticos y el contenido se muestra lo antes posible, el usuario podrá navegar por el sitio web e ignorar cualquier estilo de presentación que falte (como fuentes), es decir, si no acceder al sitio web. forma del contenido.
La forma óptima de cargar fuentes.
No tiene sentido reinventar la rueda aquí. Harry Roberts ya ha hecho un gran trabajo al describir una forma óptima de cargar fuentes web. Entra en gran detalle con una investigación exhaustiva y datos de Google Fonts, resumiéndolo todo en un proceso de cuatro pasos:
- Preconéctese al origen del archivo de fuente.
- Precargue la hoja de estilo de fuente de forma asincrónica con baja prioridad.
- Cargue de forma asincrónica la hoja de estilo de fuente y el archivo de fuente después de que el contenido se haya renderizado con JavaScript.
- Proporciona una fuente alternativa para los usuarios con JavaScript desactivado.
Implementamos nuestra fuente usando el enfoque de Harry:
!-- https://fonts.gstatic.com is the font file origin --!-- It may not have the same origin as the CSS file (https://fonts.googleapis.com) --link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /!-- We use the full link to the CSS file in the rest of the tags --link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Merriweatherdisplay=swap" /link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Merriweatherdisplay=swap" media="print" onload="this.media='all'" /noscript link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Merriweatherdisplay=swap" //noscript
Observe el media="print"
enlace en la hoja de estilo de fuente. Los navegadores otorgan automáticamente baja prioridad a las hojas de estilo de impresión y las excluyen como parte de la ruta de procesamiento crítico. Una vez cargada la hoja de estilos de impresión, onload
se activa un evento, el medio cambia a un all
valor predeterminado y la fuente se aplica a todos los tipos de medio (pantalla, impresión y voz).
Es importante tener en cuenta que el autohospedaje de las fuentes también puede ayudar a solucionar problemas de bloqueo de renderizado, pero esa no siempre es una opción. El uso de una CDN, por ejemplo, puede resultar inevitable. En algunos casos, es beneficio dejar que una CDN haga el trabajo pesado cuando se trata de servir recursos estáticos.
Aunque ahora estamos cargando la hoja de estilo de fuente y los archivos de fuente en la forma óptima sin bloqueo de renderizado, hemos introducido un problema menor de UX...
Flash de texto sin estilo (FOUT)
Esto es lo que llamamos FOUT:
¿Por qué sucede eso? Para eliminar un recurso que bloquea el procesamiento, debemos cargarlo después de que el contenido de la página se haya procesado (es decir, se haya mostrado en la pantalla). En el caso de una hoja de estilo de fuente de baja prioridad que se carga de forma asíncrona después de recursos críticos, el usuario puede ver el momento en que la fuente cambia de la fuente alternativa a la fuente descargada. No solo eso, el diseño de la página puede cambiar, lo que hace que algunos elementos parezcan rotos hasta que se carga la fuente web.
La mejor manera de lidiar con FOUT es hacer que la transición entre la fuente alternativa y la fuente web sea fluida. Para lograrlo necesitamos:
- Elija una fuente de sistema alternativa adecuada que coincida lo más posible con la fuente cargada de forma asincrónica.
- Ajuste los estilos de fuente (
font-size
,line-height
,letter-spacing
, etc.) de la fuente alternativa para que coincidan lo más posible con las características de la fuente cargada asincrónicamente. - Borre los estilos de la fuente alternativa una vez que se haya procesado el archivo de fuente cargado asincrónicamente y aplique los estilos previstos para la fuente recién cargada.
Podemos usar Font Style Matcher para encontrar fuentes óptimas del sistema de reserva y configurarlas para cualquier fuente web que planeemos usar. Una vez que tengamos listos los estilos tanto para la fuente alternativa como para la fuente web, podemos pasar al siguiente paso.
Podemos usar la API de carga de fuentes CSS para detectar cuándo se ha cargado nuestra fuente web. ¿Por qué eso? El cargador de fuentes web de Typekit alguna vez fue una de las formas más populares de hacerlo y, si bien es tentador continuar usándolo o bibliotecas similares, debemos considerar lo siguiente:
- No se ha actualizado durante más de cuatro años , lo que significa que si algo falla en el complemento o se requieren nuevas funciones, es probable que nadie las implemente ni las mantenga.
- Ya estamos manejando la carga asíncrona de manera eficiente usando el fragmento de Harry Roberts y no necesitamos depender de JavaScript para cargar la fuente.
Si me preguntas, usar una biblioteca tipo Typekit es demasiado JavaScript para una tarea simple como esta. Quiero evitar el uso de bibliotecas y dependencias de terceros, así que implementemos la solución nosotros mismos e intentemos que sea lo más simple y directa posible, sin aplicar demasiada ingeniería.
Aunque la API de carga de fuentes CSS se considera tecnología experimental, tiene aproximadamente un 95% de compatibilidad con el navegador. Pero independientemente, debemos proporcionar una alternativa si la API cambia o queda obsoleta en el futuro. El riesgo de perder una fuente no vale la pena.
La API de carga de fuentes CSS se puede utilizar para cargar fuentes de forma dinámica y asincrónica. Ya hemos decidido no depender de JavaScript para algo tan sencillo como la carga de fuentes y lo hemos solucionado de forma óptima utilizando HTML plano con precarga y preconexión. Usaremos una única función de la API que nos ayudará a comprobar si la fuente está cargada y disponible.
document.fonts.check("12px 'Merriweather'");
La check()
función devuelve true
o false
dependiendo de si la fuente especificada en el argumento de la función está disponible o no. El valor del parámetro de tamaño de fuente no es importante para nuestro caso de uso y se puede establecer en cualquier valor. Aún así, debemos asegurarnos de que:
- Tenemos al menos un elemento HTML en una página que contiene al menos un carácter con una declaración de fuente web aplicada. En los ejemplos, usaremos
nbsp;
pero cualquier personaje puede hacer el trabajo siempre que esté oculto (sin usardisplay: none;
) tanto para los usuarios videntes como para los no videntes. La API rastrea los elementos DOM a los que se les aplican estilos de fuente. Si no hay elementos coincidentes en una página, entonces la API no podrá determinar si la fuente se ha cargado o no. - La fuente especificada en el
check()
argumento de la función es exactamente como se llama la fuente en CSS.
Implementé el detector de carga de fuentes usando la API de carga de fuentes CSS en la siguiente demostración. Por ejemplo, la carga de fuentes y su escucha se inician haciendo clic en el botón para simular la carga de una página para que pueda ver cómo se produce el cambio. En proyectos normales, esto debería suceder poco después de que el sitio web se haya cargado y renderizado.
¿No es eso asombroso? Nos tomó menos de 30 líneas de JavaScript implementar un detector de carga de fuentes simple, gracias a una función bien soportada de la API de carga de fuentes CSS. También hemos manejado dos posibles casos extremos en el proceso:
- Algo sale mal con la API o se produce algún error que impide que se cargue la fuente web.
- El usuario navega por el sitio web con JavaScript desactivado.
Ahora que tenemos una manera de detectar cuándo el archivo de fuente ha terminado de cargarse, necesitamos agregar estilos a nuestra fuente alternativa para que coincida con la fuente web y ver cómo manejar FOUT de manera más efectiva.
¡La transición entre la fuente alternativa y la fuente web parece fluida y hemos logrado lograr un FOUT mucho menos notable! En un sitio complejo, este cambio daría como resultado menos cambios de diseño y los elementos que dependen del tamaño del contenido no se verían rotos o fuera de lugar.
¿Qué está pasando debajo del capó?
Echemos un vistazo más de cerca al código del ejemplo anterior, comenzando con el HTML. Tenemos el fragmento en el head
elemento, lo que nos permite cargar la fuente de forma asincrónica con precarga, preconexión y respaldo.
body !-- ... Website content ... -- div aria-visibility="hidden" /* There is a non-breaking space here */ /div script document.getElementsByTagName("body")[0].classList.remove("no-js"); /script/body
Observe que tenemos una .no-js
clase codificada en el body
elemento, que se elimina en el momento en que el documento HTML termina de cargarse. Esto aplica estilos de fuente web para usuarios con JavaScript deshabilitado.
En segundo lugar, ¿recuerda que la API de carga de fuentes CSS requiere al menos un elemento HTML con un solo carácter para rastrear la fuente y aplicar sus estilos? Agregamos un personaje div
con un nbsp;
carácter que ocultamos de manera accesible tanto para usuarios videntes como para no videntes, ya que no podemos usar display: none;
. Este elemento tiene un font-family: 'Merriweather'
estilo en línea. Esto nos permite cambiar suavemente entre los estilos alternativos y los estilos de fuente cargados, y asegurarnos de que se realice un seguimiento adecuado de todos los archivos de fuentes, independientemente de si se utilizan en la página o no.
Tenga en cuenta que el nbsp;
personaje no aparece en el fragmento de código, ¡pero está ahí!
El CSS es la parte más sencilla. Podemos utilizar las clases CSS que están codificadas en HTML o aplicadas condicionalmente con JavaScript para manejar varios estados de carga de fuentes.
body:not(.wf-merriweather--loaded):not(.no-js) { font-family: [fallback-system-font]; /* Fallback font styles */}
.wf-merriweather--loaded,.no-js { font-family: "[web-font-name]"; /* Webfont styles */}
/* Accessible hiding */.hidden { position: absolute; overflow: hidden; clip: rect(0 0 0 0); height: 1px; width: 1px; margin: -1px; padding: 0; border: 0; }
JavaScript es donde ocurre la magia. Como se describió anteriormente, estamos verificando si la fuente se ha cargado usando la check()
función de la API de carga de fuentes CSS. Nuevamente, el parámetro de tamaño de fuente puede tener cualquier valor (en píxeles); es el valor de la familia de fuentes el que debe coincidir con el nombre de la fuente que estamos cargando.
var interval = null;
function fontLoadListener() { var hasLoaded = false;
try { hasLoaded = document.fonts.check('12px "[web-font-name]"') } catch(error) { console.info("CSS font loading API error", error); fontLoadedSuccess(); return; } if(hasLoaded) { fontLoadedSuccess(); }}
function fontLoadedSuccess() { if(interval) { clearInterval(interval); } /* Apply class names */}
interval = setInterval(fontLoadListener, 500);
Lo que sucede aquí es que estamos configurando nuestro oyente para fontLoadListener()
que se ejecute a intervalos regulares. Esta función debe ser lo más simple posible para que se ejecute de manera eficiente dentro del intervalo. Estamos utilizando el bloque try-catch para manejar cualquier error y detectar cualquier problema de modo que los estilos de fuente web sigan aplicándose en el caso de un error de JavaScript para que el usuario no experimente ningún problema con la interfaz de usuario.
A continuación, tenemos en cuenta cuándo se carga correctamente la fuente con fontLoadedSuccess()
. Primero debemos asegurarnos de borrar el intervalo para que la verificación no se ejecute innecesariamente después de él. Aquí podemos agregar los nombres de clases que necesitamos para aplicar los estilos de fuente web.
Y, por fin, estamos iniciando el intervalo. En este ejemplo, lo configuramos en 500 ms, por lo que la función se ejecuta dos veces por segundo.
Aquí hay una implementación de Gatsby.
Gatsby hace algunas cosas que son diferentes en comparación con el desarrollo web básico (e incluso con la pila tecnológica habitual de crear-reaccionar-aplicación), lo que hace que la implementación de lo que hemos cubierto aquí sea un poco complicada.
Para facilitar esto, desarrollaremos un complemento Gatsby local, de modo que todo el código relevante para nuestro cargador de fuentes se encuentre en plugins/gatsby-font-loader
el siguiente ejemplo.
Nuestro código y configuración del cargador de fuentes se dividirán en los tres archivos principales de Gatsby:
- Configuración del complemento (
gatsby-config.js
): Incluiremos el complemento local en nuestro proyecto, enumeraremos todas las fuentes locales y externas y sus propiedades (incluido el nombre de la fuente y la URL del archivo CSS) e incluiremos todas las URL de preconexión. - Código del lado del servidor (
gatsby-ssr.js
): usaremos la configuración para generar e incluir etiquetas de precarga y preconexión en el HTMLhead
usandosetHeadComponents
la función de la API de Gatsby. Luego, generaremos los fragmentos de HTML que ocultan la fuente y los incluiremos en HTML usandosetPostBodyComponents
. - Código del lado del cliente (
gatsby-browser.js
): dado que este código se ejecuta después de que se haya cargado la página y después de que se inicie React, ya es asíncrono. Eso significa que podemos inyectar los enlaces de la hoja de estilo de fuente usando reaccionar-casco. También iniciaremos un detector de carga de fuentes para lidiar con FOUT.
Puede consultar la implementación de Gatsby en el siguiente ejemplo de CodeSandbox.
Lo sé, algunas de estas cosas son complejas. Si solo desea una solución sencilla para la carga de fuentes asincrónica y de alto rendimiento y eliminación de FOUT, he desarrollado un complemento gatsby-omni-font-loader solo para eso. Utiliza el código de este artículo y lo mantengo activamente. Si tiene sugerencias, informes de errores o contribuciones de código, no dude en enviarlos a GitHub.
Conclusión
El contenido es quizás el componente más importante de la experiencia de un usuario en un sitio web. Necesitamos asegurarnos de que el contenido tenga la máxima prioridad y se cargue lo más rápido posible. Eso significa utilizar estilos de presentación mínimos (es decir, CSS crítico incorporado) en el proceso de carga. Es también por eso que las fuentes web se consideran no críticas en la mayoría de los casos (el usuario aún puede consumir el contenido sin ellas), por lo que está perfectamente bien que se carguen después de que la página se haya renderizado.
Pero eso podría llevar a FOUT y cambios de diseño, por lo que el detector de carga de fuentes es necesario para realizar un cambio fluido entre la fuente alternativa del sistema y la fuente web.
¡Me gustaría escuchar tu opinión! Déjame saber en los comentarios cómo estás abordando el problema de la carga de fuentes web, los recursos de bloqueo de renderizado y FOUT en tus proyectos.
Referencias
- Eliminar los recursos que bloquean el renderizado (web.dev)
- Optimice la carga y renderizado de WebFont (web.dev)
- Bloqueo de renderizado CSS (Fundamentos web de Google)
- Las fuentes de Google más rápidas (magia CSS)
- Conceptos básicos de CSS: pilas de fuentes alternativas para una tipografía web más sólida (trucos CSS)
- API de carga de fuentes CSS (MDN)
- Comparador de estilos de fuente
Deja un comentario