Como desarrollador web enfocado en la optimización de las páginas web que he creado, siempre he intentado evitar la sobrecarga de recursos en exceso. Por lo general, la carga de fuentes es, junto a las imágenes, uno de los recursos que suele tener gran peso en el diseño. El inconveniente es que también lo tiene en la performance de la web en cuestión.

Desde hace semanas (o meses) quería escribir sobre este tema, aunque pensaba que en ¡2019! poco podía aportar ya a la causa con todo lo que se había escrito sobre este asunto. Hasta que me encontré con este tweet en timeline:

Self hosting your web fonts is the best choice for performance (maybe not for your wallet though)

5:35 - 13 ago. 2019 @zachleat

También, y como decía al principio, preocupado por la optimización de carga de la web, suelo usar la herramienta Pagespeed Insights de Google para hacer un análisis rápido de las web con las que debo trabajar. Uno de las cosas que me suelo encontrar es este mensaje:

Advertencia sobre fuentes de Pagespeed Insights

Con todo esto voy a dejar escrito lo que para mi ha funcionado, llegando a un equilibrio entre diseño y performance:

  1. Cómo cargar una familia de fuentes
  2. Cómo cargar más rápido desde Google Fonts
  3. Control del comportamiento de la fuente con font-display
  4. Cuándo cargar la fuente para no bloquear la composición de la página
  5. Cachear fuentes desde el servidor

Cómo cargar una familia de fuentes

Definir una fuente en nuestro CSS

Primero veamos como definir una fuente en nuestro CSS y con que propiedades o descriptores contamos y sus valores.

@font-face {
    font-family: 'FontName';
    font-display: auto | block | swap | fallback;
    font-style: normal | italic | oblique | initial | inherit;
    font-weight: normal | bold | bolder | lighter | number | initial | inherit;
    src: local('Font Name'), url(path-to-font-file) format('eot | ttf | woff | woff2');
    unicode-range: U+0000-00FF;
}

Información sobre descriptores obligatorios y opcionales

Del código anterior debemos saber que las propiedades obligatorias son:

  1. font-family: Definimos el nombre que usaremos en nuestra hoja de estilos.
  2. src: Indicamos la ruta (path) en la que se encuentra nuestra fuente y su formato. En este atributo podemos indicar mucha información respecto a la fuente.  Veremos más adelante esta información.

El resto de propiedades son opcionales. Veamoslas:

  1. font-style: Definimos el estilo de una familia de fuentes. El valor por defecto será normal. Puedes ampliar esta explicación en font-style de MDN web docs.
  2. font-weight: Definimos el peso o grueso de una familia de fuentes. El valor por defecto será normal. Puedes ampliar esta explicación en font-weight de MDN web docs.
  3. unicode-range: Definimos el rango de caracteres que la familia de fuentes usará en la web. Si en el documento no usara ningún carácter del rango, la fuente no será descargada. En caso de usar un solo carácter, la fuente será descargada para su uso. Puedes ampliar la explicación en unicode-range de MDN web docs.
  4. font-display: Definimos el comportamiento de la fuente durante la carga. El valor por defecto es auto. Puedes ampliar esta explicación en font-display de MDN web docs. Más abajo ampliaré información sobre la importancia de este atributo.

Atributo src

En esta propiedad vamos a indicar bastante información. Debes saber que este descriptor no tiene porqué ser único. Puedes definir más de uno en la declaración de @font-face. Puedes ampliar información sobre este atributo en src de font-face en MDN web docs.

En el ejemplo anterior tenemos una lista de valores separados por comas. Estos formarán los valores para el descriptor. Veamoslos en detalle:

local

Indicamos al cliente (navegador) que busque si existe la fuente en el sistema. Podemos indicar varios local separados por comas, para indicar diferentes nombres de la misma fuente. Así, es bastante usual encontrase algo como lo siguiente:

src: local('Lato Regular'), local('Lato-Regular'), ...​

url y format

Indicamos la ruta donde está alojada la fuente y su formato. El fichero de la fuente podemos tenerlo alojado en nuestro servidor. También podemos usar un servicio de terceros y usar la URL al fichero de la fuente. Por ejemplo:

<!-- en tu servidor -->
src: local('Lato Regular'), local('Lato-Regular'), url(../fonts/lato.woff2) format('woff2');
<!-- desde google fonts -->
src: local('Lato Regular'), local('Lato-Regular'), url(https://fonts.gstatic.com/s/lato/v13/1YwB1sO8YE1Lyjf12WNiUA.woff2) format('woff2');​

Será muy habitual encontrar en esta propiedad una lista separada por comas indicando diferentes ficheros y formatos. Esto es debido a que cada navegador usaba un formato diferente de la fuente. Por suerte, ultimamente parece que se ha estandarizado el formato woff. Aún así es bastante normal encontrar este tipo de declaraciones:

src: local('Lato Regular'),
         url('lato-regular.eot?#iefix') format('embedded-opentype'),
         url('lato-regular.woff') format('woff'),
         url('lato-regular.ttf') format('truetype'),
         url('lato-regular.woff') format('woff'),
         url('lato-regular.woff2') format('woff2');

Sabiendo todo esto, veamos como definir una familia de fuentes.

Definir una familia de fuentes

Teniendo la información anterior veamos un ejemplo de como definir la familia de una fuente. Entendamos la familia de una fuente los diferentes ficheros que darán los diferentes estilos a la fuente. Veamos como realizar la carga de la fuente Lato en diferentes versiones, es decir: regular, itálica, negrita.

<!-- Definir familia de fuentes Lato -->
<!-- Regular -->
@font-face {
    font-family: 'Lato';
    font-display: fallback;
    font-style: normal;
    font-weight: 400;
    src: local('Lato Regular'), local('Lato-Regular'), url(../fonts/lato-regular.woff2) format('woff2');
}
<!-- Bold (negrita) -->
@font-face {
    font-family: 'Lato';
    font-display: fallback;
    font-style: normal;
    font-weight: bold;
    src: local('Lato Bold'), local('Lato-Bold'), url(../fonts/lato-bold.woff2) format('woff2');
}
<!-- Italic (cursiva) -->
@font-face {
    font-family: 'Lato';
    font-display: fallback;
    font-style: italic;
    font-weight: 400;
    src: local('Lato Italic'), local('Lato-Italic'), url(../fonts/lato-italic.woff2) format('woff2');
}

Podemos comprobar que el font-family es igual para los tres. Si analizamos el código anterior, lo que cambia respecto a una u otra son los siguientes atributos: font-style, font-weight y src.

Con esto y definiendo en nuestra etiqueta body la fuente que queremos usar de la siguiente manera:

body {
    font-family: 'Lato', Helvetica, Verdana, sans-serif;
}

los diferentes elementos HTML, usarán la fuente Lato. ¿Qué quiero decir con esto? Los elementos que por defecto se muestran en negrita se mostrarán con la fuente Lato Bold y los elementos que por defecto se muestran en itálica se mostrarán con la fuente Lato Italic. ¿Qué elementos son? Por ejemplo las etiquetas de encabezados hx, b y strong y las etiqueta em como negrita e itálica respectivamente. ¿Por qué comento esto? Me encuentro, y yo mismo he usado por desconocimiento, la siguiente forma de definir una familia de fuentes:

<!-- Definir familia de fuentes Lato -->
<!-- Regular -->
@font-face {
    font-family: 'Lato';
    font-display: fallback;
    font-style: normal;
    font-weight: 400;
    src: local('Lato Regular'), local('Lato-Regular'), url(../fonts/lato-regular.woff2) format('woff2');
}
<!-- Bold (negrita) -->
@font-face {
    font-family: 'Lato Bold';
    font-display: fallback;
    font-style: normal;
    font-weight: 400;
    src: local('Lato Bold'), local('Lato-Bold'), url(../fonts/lato-bold.woff2) format('woff2');
}
<!-- Italic (cursiva) -->
@font-face {
    font-family: 'Lato Italic';
    font-display: fallback;
    font-style: normal;
    font-weight: 400;
    src: local('Lato Italic'), local('Lato-Italic'), url(../fonts/lato-italic.woff2) format('woff2');
}

Esto me obligaba a redifinir los elementos genéricos HTML de esta forma:

body {
    font-family: 'Lato', Helvetica, Verdana, sans-serif;
}

h1, h2, h3, h4, h5, h6, strong {
    font-family: 'Lato Bold';
    font-weight: normal;
}

em {
    font-family: 'Lato Italic';
    font-style: normal;
}

.outstanding {
    font-family: 'Lato Bold';
}

Los inconveniente de definir una familia de fuentes de esta manera son:

  1. Al resetear estilos para que la fuente se muestre como deseamos, si por cualquier motivo la fuente no fuera cargada por el navegador, perderemos la visualización de los distintos elementos en sus diferentes estilos: ni las negritas ni las cursivas se verían como tal.
  2. Al usar el descriptor font-family más a menudo, añadimos complejidad al mantenimiento de la hoja de estilos. La simple decisión de cambiar de familia será más compleja de ejecutar.

Cómo cargar más rápido desde Google Fonts

Vimos un ejemplo anterior en el  que podemos cargar fuentes desde un recurso externo. Esto quiere decir que podríamos llamar a una URL externa a nuestro dominio. Siguiendo con el ejemplo anterior de la carga de la familia Lato en sus tres variantes: regular, itálica y negrita, veamos como aprovechar Google Fonts para la carga de una familia.

También haré uso de la priorización de la carga de recursos con dns-prefetch y prefetch. Recomiendo leer antes este artículo: Prioridades de recursos: cómo hacer que el navegador te ayude antes de continuar.

¿Ya lo has leído? Continuemos entonces.

Obtener la familia de Google Fonts

Google Fonts nos ofrece de forma sencilla este enlace para incorporarlo al head de nuestro HTML:

<head>
<link href="https://fonts.googleapis.com/css?family=Lato:400,400i,700&display=swap" rel="stylesheet"> 
</head>

Si curioseamos el enlace que nos indica: https://fonts.googleapis.com/css?family=Lato:400,400i,700&display=swap veremos esto:

/* latin-ext */
@font-face {
  font-family: 'Lato';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: local('Lato Italic'), local('Lato-Italic'), url(https://fonts.gstatic.com/s/lato/v16/S6u8w4BMUTPHjxsAUi-qJCY.woff2) format('woff2');
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Lato';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: local('Lato Italic'), local('Lato-Italic'), url(https://fonts.gstatic.com/s/lato/v16/S6u8w4BMUTPHjxsAXC-q.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'Lato';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: local('Lato Regular'), local('Lato-Regular'), url(https://fonts.gstatic.com/s/lato/v16/S6uyw4BMUTPHjxAwXjeu.woff2) format('woff2');
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Lato';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: local('Lato Regular'), local('Lato-Regular'), url(https://fonts.gstatic.com/s/lato/v16/S6uyw4BMUTPHjx4wXg.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
  font-family: 'Lato';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: local('Lato Bold'), local('Lato-Bold'), url(https://fonts.gstatic.com/s/lato/v16/S6u9w4BMUTPHh6UVSwaPGR_p.woff2) format('woff2');
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Lato';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: local('Lato Bold'), local('Lato-Bold'), url(https://fonts.gstatic.com/s/lato/v16/S6u9w4BMUTPHh6UVSwiPGQ.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Nos ofrece dos versiones para cada estilo. En la mayoría de los casos, con tener una versión será suficiente. Así que de lo anterior me voy a quedar con la versión latin-ext de cada estilo:

/* latin-ext */
@font-face {
  font-family: 'Lato';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: local('Lato Italic'), local('Lato-Italic'), url(https://fonts.gstatic.com/s/lato/v16/S6u8w4BMUTPHjxsAUi-qJCY.woff2) format('woff2');
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin-ext */
@font-face {
  font-family: 'Lato';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: local('Lato Regular'), local('Lato-Regular'), url(https://fonts.gstatic.com/s/lato/v16/S6uyw4BMUTPHjxAwXjeu.woff2) format('woff2');
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin-ext */
@font-face {
  font-family: 'Lato';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: local('Lato Bold'), local('Lato-Bold'), url(https://fonts.gstatic.com/s/lato/v16/S6u9w4BMUTPHh6UVSwaPGR_p.woff2) format('woff2');
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

En lugar de incluir el link que nos proporciona Google Fonts en nuestro head, voy a incorporar a mi CSS la definición de la familia Lato tal y como véis encima de estas líneas.

Por último, me gustaría añadir la ventaja de incluir la fuente desde un recurso externo. El usarlo, además de tener menos descargas desde nuestro servidor (ahorro de dinero) nos ofrece que en caso de que el usuario haya visitado una página anterior que use el mismo recurso externo, este no será descargado de nuevo en su ordenador, si no que usará el ya descargado con anterioridad.

También tiene sus inconvenientes. Si el servicio no está disponible, fallará la descarga y no podremos usar dicha fuente.

Acelerar la resolución de dominio con dns-prefetch

Si has leído el artículo que recomendé más arriba, seguramente ya sabrás que hace. En todo caso esto consiste en indicar al cliente (navegador) que vamos a conectar con un dominio diferente. Al estar preparado, este trabajará más rápido.

Veamos como indicarlo con un ejemplo:

<head>
  <link rel="dns-prefetch" href="//fonts.googleapis.com" />
</head>

Esta declaración que hacemos en el head de nuestro HTML deberíamos hacerla antes de llamar a cualquier recurso del dominio que hemos indicado. En nuestro ejemplo realizamos la llamada a los recursos en nuestro CSS. Esta declaración debe ir antes de llamar a la hoja de estilos.

Quizá sea obvio, pero se pueden implementar tantos dns-prefetch como llamadas a recursos externos tengamos.

Precarga de las fuentes

Poco que añadir a lo comentado en el artículo recomendado. En él, explican en Caso de Uso: Fuentes como realizar una precarga de estas. Veamos como hacerlo en nuestro caso particular:

<head>
  <!-- dns-prefetch -->
  <link rel="dns-prefetch" href="//fonts.googleapis.com" />

  <!-- preload -->
  <link rel="preload" href="//fonts.gstatic.com/s/lato/v16/S6u8w4BMUTPHjxsAUi-qJCY.woff2" as="font" crossorigin="crossorigin" type="font/woff2">
  <link rel="preload" href="//fonts.gstatic.com/s/lato/v16/S6uyw4BMUTPHjxAwXjeu.woff2" as="font" crossorigin="crossorigin" type="font/woff2">
  <link rel="preload" href="//fonts.gstatic.com/s/lato/v16/S6u9w4BMUTPHh6UVSwaPGR_p.woff2" as="font" crossorigin="crossorigin" type="font/woff2">

  <!-- load css -->
  <link rel="stylesheet" type="text/css" href="style.css" media="screen" />
</head>

Se debe tener en cuenta la importancia del atributo crossorigin en este caso. Tal como indican en el artículo y que cito aquí:

Ten en cuenta que el uso de crossorigin aquí es importante. Sin este atributo, el navegador ignora la fuente precargada y se realiza una nueva operación fetch. Esto es porque se espera que el navegador obtenga las fuentes anónimamente y la solicitud de precarga se hace anónima solo mediante el atributo crossorigin

Si piensas al igual que yo, que la carga de la fuente no es algo crítico, en lugar de usar preload podrías usar prefetch. En este caso indicamos, o más bien, dejamos decidir al navegador la importancia en la resolución del recurso. Diferente a preload, el cual indica de forma explícita la importancia de carga.

<head>
  <!-- dns-prefetch -->
  <link rel="dns-prefetch" href="//fonts.googleapis.com" />

  <!-- preload -->
  <link rel="prefetch" href="//fonts.gstatic.com/s/lato/v16/S6u8w4BMUTPHjxsAUi-qJCY.woff2" as="font" crossorigin="crossorigin" type="font/woff2">
  <link rel="prefetch" href="//fonts.gstatic.com/s/lato/v16/S6uyw4BMUTPHjxAwXjeu.woff2" as="font" crossorigin="crossorigin" type="font/woff2">
  <link rel="prefetch" href="//fonts.gstatic.com/s/lato/v16/S6u9w4BMUTPHh6UVSwaPGR_p.woff2" as="font" crossorigin="crossorigin" type="font/woff2">

  <!-- load css -->
  <link rel="stylesheet" type="text/css" href="style.css" media="screen" />
</head>

Control del comportamiento con font-display

Si recordamos la imagen de más arriba, en la que Pagespeed Insights recomienda que revisemos la estrategia de carga de fuentes, nos ofrece un link para documentarnos al respecto: Controlling Font Performance with font-display. En él explica en qué consiste esta propiedad. Si prefieres leerlo en español puedes ver font-display de MDN web doc.

Veamos, la teoría que nos ofrecen es que en el proceso de intentar cargar una fuente, este se divide en tres fases:

  1. Momento de bloqueo: En el cual, si la fuente a usar no está cargada, se usa una alternativa.
  2. Cambio de fuente: En el cual, si la fuente a usar no está cargada, se usa una alternativa.
  3. Fallo de carga: En el cual, si la fuente a usar falla, se usa una alternativa.

Con font-display podemos influir en el comportamiento indicando el valor que deseamos. Los valores son:

  1. auto: Depende del navegador y es el valor por defecto.
  2. block: Establece un tiempo de bloqueo de la fuente corto y un periodo de intercambio infinito.
  3. swap: No establece tiempo de bloqueo para la fuente y un tiempo infinito de intercambio.
  4. fallback: Establece un tiempo de bloqueo muy pequeño y un período de intercambio corto.
  5. optional: Establece un tiempo de bloqueo muy corto y sin tiempo de intercambio.

¿Qué significa todo esto y cuál usar? Depende de si para tu web es imprescindible cargar la fuente o es un molaría pero no es imprescindible.

En el primero de los casos, en el que es imprescindible, deberías usar el valor block, que es el que usan la mayoría de navegadores con su valor a auto, o swap. En el caso de swap cargará la fuente tan rápido la fuente esté cargada. Estos casos parecen ser recomendados si usas la fuente para la marca de la página o elementos imprescindibles.

En el segundo de los casos, molaría pero no es imprescindible, deberías usar el valor fallback u optional. Los tiempo de bloque son de unos cien milisegundos, mientras mostrará la alternativa de la que disponga el navegador.

Google Fonts actualmente nos ofrece, por defecto, las fuentes con la declaración font-display: swap. Además, debemos tener en cuenta, si nos importa mucho la performance y lo que diga Pagespeed Insights al respecto, que la advertencia de la imagen inicial se produce cuando, al cargar la web, la fuente no se visualiza durante un determinado periodo de tiempo o es invisible. Podemos aprender más sobre estos efectos en el artículo: FOUT, FOIT, FOFT.

Cuándo cargar las fuentes para no bloquear el dibujado de la página

Como hemos ido viendo a lo largo del artículo, casi todo consiste en estrategias de carga de fuentes. Al igual que otros recursos se pueden cargar de forma asíncrona, con las fuentes o quizá mejor con un fichero donde solo tengamos las fuentes podríamos hacer lo mismo mediante el uso de JavaScript. Aunque quizá, y si para nuestra web no es tan imprescindible la carga de fuentes en un primer momento podríamos retrasar la carga al igual que historicamente se ha hecho con recursos JavaScript. Añadiendo el fichero CSS correspondiente, el que contiene la definición de las fuentes, al final de la página justo antes del cierre del body.

Veamos como:

<html>
<head>
  <!-- critical css -->
  <link rel="stylesheet" type="text/css" href="style.css" media="screen" />
<head>
<body>
  <!-- header, main and footer -->

  <!-- load no critical css -->
  <link rel="stylesheet" type="text/css" href="style-no-critical.css" media="screen" property="stylesheet"/>

  <!-- load js -->
  <script type="text/javascript" src="app.js" async></script>
</body>
</html>

He añadido la propiedad property con el valor stylesheet para que los validadores de HTML entiendan que se refiere a una hoja de estilos.

Esta estrategia, además de para familias de fuentes, la he usado para imágenes de fondo que debían ser de gran calidad (con su correspondiente tamaño) evitando bloquear la página al comienzo. En este caso es recomendable incluir el link al CSS antes que la carga de cualquier fichero o script JavaScript.

Cachear las fuentes desde el servidor

Por último, no debemos olvidar cachear las fuentes desde nuestro servidor. En el caso de Apache 2.x desde el fichero htacces que nos permita configurar (dependerá de tu proveedor de hosting) deberíamos incluir las siguientes líneas (entre otras):

<ifmodule mod_deflate.c>
  #Checks if your server supports Addtype
  <ifmodule mod_mime.c>
    Addtype font/opentype .otf
    Addtype font/eot .eot
    Addtype font/truetype .ttf
    Addtype font/woff .woff
  </ifmodule>
  AddOutputFilterByType DEFLATE font/opentype font/truetype font/eot
</ifmodule>

# Expires Headers - 2678400s = 31 days
<ifmodule mod_expires.c>
  ExpiresActive On
  ExpiresDefault "access plus 1 year"

  # Webfonts
  ExpiresByType font/truetype "access plus 1 year"
  ExpiresByType font/opentype "access plus 1 year"
  ExpiresByType font/woff "access plus 1 year"
  ExpiresByType image/svg+xml "access plus 1 year"
  ExpiresByType application/vnd.ms-fontobject "access plus 1 year"
</ifmodule>

Un tiempo largo es el recomendado para ofrecer una buena experiencia según Pagespeed Insights para los recursos que no suelen cambiar como es el caso de las fuentes.

La mejor estrategia para las familias de fuentes...

obviamente es evitarlas o mantener un número bajo de estas. Si por especificaciones del proyecto debes incluir demasiadas fuentes, esto podrá ayudar, pero con un éxito limitado. Algo que merece la pena probar (o volver a probar) es el uso de fuentes del sistema.

Recursos