font-display: los cinco valores y sus periodos exactos
El periodo de bloqueo y el periodo de intercambio con sus duraciones en milisegundos, qué hace cada uno de los cinco valores, y el criterio para elegir según qué papel juegue la fuente en la página.
Todo lo que ocurre entre que el navegador pide la fuente y que la tiene está gobernado por un solo descriptor de cinco valores. La documentación popular los describe con adjetivos —bloquea un poco, intercambia rápido— cuando en realidad la especificación define dos periodos con duraciones recomendadas concretas, y los motores las implementan con números que se pueden citar. Conocer esos números es la diferencia entre elegir un valor y copiarlo de un ejemplo.
- Definir el periodo de bloqueo y el de intercambio con sus duraciones en milisegundos.
- Enumerar los cinco valores con el par de periodos que le corresponde a cada uno.
- Distinguir el destello de texto invisible del destello de texto sin estilar.
- Elegir el valor correcto según el papel de la fuente en la página.
Los dos periodos
Desde el instante en que se solicita la fuente, su ciclo de vida se divide en tres tramos consecutivos, y el descriptor solo controla la duración de los dos primeros.
El periodo de bloqueo. Si la fuente no ha llegado, el elemento se renderiza con una cara de reserva invisible. Ocupa el espacio que ocuparía la fuente de reserva, pero no se dibuja nada. El texto está ahí, es seleccionable y es accesible, y no se ve. Si la fuente llega dentro de este periodo, se usa con normalidad y el usuario no ha visto nada raro.
El periodo de intercambio. Empieza cuando acaba el de bloqueo. Si la fuente no ha llegado, el texto se pinta con la fuente de reserva, visible. Si la fuente llega durante este periodo, se intercambia en cuanto llega.
El periodo de fallo. Empieza cuando acaba el de intercambio. Si la fuente no ha llegado, el navegador la trata como no disponible y usa la reserva para el resto de la vida de la página, aunque la fuente llegue después. La descarga puede completarse y quedar en caché para la siguiente navegación, pero en esta ya no se usa.
Los dos fenómenos que todo el mundo nombra son consecuencia directa de estos tramos. El destello de texto invisible es un periodo de bloqueo largo: el usuario ve un hueco en blanco donde debería haber texto. El destello de texto sin estilar es un periodo de intercambio activo: el usuario ve el texto con la fuente equivocada y después cambia. El segundo es peor para el CLS y mucho mejor para la percepción de velocidad, y esa es la tensión que resuelve el descriptor.
Los cinco valores
La especificación describe cada valor como un par de periodos, y recomienda duraciones concretas: tres segundos para lo que llama un periodo de bloqueo corto, y cien milisegundos o menos para lo que llama un periodo de bloqueo extremadamente pequeño. Los motores implementan esas recomendaciones.
| Valor | Periodo de bloqueo | Periodo de intercambio | Efecto visible |
|---|---|---|---|
auto |
Lo que decida el navegador | Lo que decida el navegador | En la práctica se comporta como block |
block |
3 s | Infinito | Texto invisible hasta 3 s, después reserva, e intercambia cuando llegue |
swap |
100 ms | Infinito | Reserva casi inmediata, e intercambia cuando llegue |
fallback |
100 ms | 3 s | Reserva casi inmediata, intercambia solo si llega antes de 3,1 s |
optional |
100 ms | 0 s | Reserva casi inmediata, y no intercambia nunca en esta carga |
Cuatro consecuencias que se leen directamente de la tabla y que conviene tener presentes.
auto no es neutro. El valor por defecto, el que tienes si no escribes nada, se comporta como block en los motores actuales: hasta tres segundos de texto invisible. La mayoría de los sitios que sufren texto invisible no eligieron block, simplemente no escribieron el descriptor.
El periodo de bloqueo retrasa el primer pintado con contenido. El texto invisible no cuenta como contenido pintado. Con block, en una conexión donde la fuente tarda 2,5 segundos, el primer pintado con contenido de un bloque de texto se retrasa 2,5 segundos. Si el elemento mayor con contenido de tu página es texto, el retraso se traslada íntegro a esa métrica también.
El periodo de intercambio infinito garantiza que se verá tu fuente, y garantiza el desplazamiento. Con swap, aunque la fuente llegue a los quince segundos, se intercambia. El usuario que ya llevaba diez segundos leyendo ve saltar el texto entero.
El periodo de intercambio de cero segundos elimina el desplazamiento por construcción. Con optional, si la fuente no está lista al terminar los 100 milisegundos de bloqueo, no se usa. Cero intercambio significa cero desplazamiento, sin ajustes ni descriptores de métrica. La descarga sigue adelante y la fuente queda en caché, así que la siguiente navegación del mismo usuario la tendrá lista dentro del periodo de bloqueo y la usará desde el primer pintado.
Todo esto se declara por cara de fuente, no por sitio, lo cual es exactamente lo que hace falta:
@font-face {
font-family: 'Titulares';
src: url('/fuentes/titulares.woff2') format('woff2');
font-weight: 600 800;
font-display: swap; /* el titular tiene que verse ya */
}
@font-face {
font-family: 'Cuerpo';
src: url('/fuentes/cuerpo.woff2') format('woff2');
font-weight: 100 900;
font-display: optional; /* el cuerpo no puede permitirse desplazar */
}
@font-face {
font-family: 'Iconos';
src: url('/fuentes/iconos.woff2') format('woff2');
font-display: block; /* un icono con la fuente de reserva es basura visual */
}
El caso de la fuente de iconos merece la explicación, porque es el único donde block es la respuesta correcta. Los glifos de una fuente de iconos viven en puntos de código que la fuente de reserva no tiene, o peor, que tiene asignados a otra cosa. Con swap, durante el periodo de intercambio el usuario ve cuadrados vacíos o letras sueltas donde deberían estar los iconos. Tres segundos de hueco invisible es mejor que tres segundos de basura. Dicho lo cual, la respuesta de verdad correcta es no usar una fuente de iconos: los mismos iconos en SVG en línea no tienen ni cadena de carga ni periodo de nada.
font-display: optional es el único valor que garantiza cero desplazamiento y cero texto invisible más allá de 100 milisegundos. Con eso sobre la mesa, la pregunta obvia es por qué no lo usa todo el mundo. La respuesta es una cifra que casi nadie mide: la tasa de acierto en la primera visita.
Cien milisegundos desde que arranca la petición son una ventana durísima. Recuerda la cadena completa: la fuente no se pide hasta que existe el árbol de renderizado, y desde ese instante hay cien milisegundos para resolver el nombre si hace falta, conectar si hace falta, pedir, recibir y validar el fichero. En una conexión móvil real, con la fuente en el mismo origen y ya conectado, un fichero de 25 KB tarda entre 120 y 250 milisegundos solo en descargarse. La tasa de acierto en la primera visita, sin más medidas, ronda el cero.
El resultado es un sitio que en la primera visita de cada usuario se ve siempre con la fuente de reserva. Si tu identidad visual depende de la tipografía, eso no es aceptable, y de ahí viene la mala fama de optional.
Ahora la parte contraintuitiva: la forma de arreglarlo es precargar la fuente, y no por la razón que parece.
La razón evidente es que precargar adelanta el inicio de la petición al momento del parseo del HTML, ganando el viaje completo de la hoja de estilos. Eso ya de por sí puede convertir un fallo seguro en un acierto probable.
Pero hay una segunda razón, específica de Chromium, que es la que cambia el juego: cuando una fuente marcada como optional está siendo precargada, el navegador retrasa deliberadamente el primer renderizado para darle la oportunidad de llegar. En lugar de pintar con la reserva y descartar la fuente, espera un poco. El resultado es que la combinación de precarga más optional tiene una tasa de acierto muy alta en la primera visita, mientras que optional a secas tiene una muy baja.
La receta completa, que es la configuración que recomiendo por defecto para la fuente del cuerpo de texto:
<link rel="preload" as="font" type="font/woff2" href="/fuentes/cuerpo.woff2" crossorigin>@font-face {
font-family: 'Cuerpo';
src: url('/fuentes/cuerpo.woff2') format('woff2');
font-weight: 100 900;
font-display: optional;
}Más la familia de reserva ajustada con los descriptores de métrica que ya sabes calcular, para que el usuario al que le falle el plazo vea una página que se parece a la buena en lugar de una desmaquetada.
Y ahora, cómo medir la tasa de acierto, porque sin ese número esto es fe:
addEventListener('load', async () => {
await document.fonts.ready;
const usada = document.fonts.check('16px "Cuerpo"');
navigator.sendBeacon('/rum-fuentes', JSON.stringify({
acierto: usada,
conexion: navigator.connection?.effectiveType ?? 'desconocida',
repetida: performance.getEntriesByType('navigation')[0]?.type === 'reload',
}));
});document.fonts.check devuelve si la familia está disponible para pintar con ese tamaño, que con optional equivale a si el plazo se cumplió. Segmentando por tipo de conexión sale la tabla que decide: si el acierto en conexiones lentas está por debajo del 50%, o aceptas que ese segmento vea la reserva —lo cual es defendible si la reserva está bien ajustada— o cambias a fallback, que intercambia si la fuente llega dentro de los primeros 3,1 segundos y no intercambia después.
fallback es, de hecho, el punto medio que más se infrautiliza de los cinco. Da el mismo arranque rápido que swap, permite el intercambio cuando llega razonablemente pronto, y corta el intercambio tardío, que es el que produce el CLS de verdad malo, el que ocurre cuando el usuario ya está leyendo. Si tuviera que elegir un solo valor sin conocer nada del sitio, elegiría ese.
El criterio de elección
Tres preguntas, en este orden.
¿Un glifo de reserva sería incorrecto en lugar de solo distinto? Es el caso de las fuentes de iconos y de los alfabetos que la reserva no cubre. Entonces block, y plantéate si esa fuente debería existir.
¿La fuente es la identidad visual del sitio y el desplazamiento es tolerable? Titulares de un sitio editorial o de marca. Entonces swap, aceptando que un intercambio tardío va a costar CLS, y compensándolo con el ajuste de la reserva.
¿El texto es contenido que el usuario va a leer de inmediato? El cuerpo de un artículo, una ficha de producto, una interfaz. Entonces optional con precarga si la tasa de acierto medida lo permite, y fallback si no.
Y el control manual, para los casos que no encajan en ninguna de las tres: la API de fuentes permite decidir tú el momento del intercambio, cargando la fuente sin que ninguna regla la reclame y aplicándola después mediante una clase.
const cara = new FontFace('Cuerpo', 'url(/fuentes/cuerpo.woff2)', { weight: '100 900' });
cara.load().then((cargada) => {
document.fonts.add(cargada);
document.documentElement.classList.add('fuente-lista');
});
/* La familia solo se aplica cuando la clase esta puesta */
.fuente-lista body { font-family: 'Cuerpo', 'Cuerpo reserva', sans-serif; }
Ese patrón permite, por ejemplo, no intercambiar si han pasado más de dos segundos, o no intercambiar si la conexión es mala, o no intercambiar si el usuario ya ha hecho scroll. Es más código y más control, y en un sitio grande suele acabar siendo la única forma de tener una política coherente.
Declara explícitamente font-display en todas tus reglas de fuente, si es que alguna no lo tiene. Después mide la tasa de acierto de optional en tu fuente de cuerpo durante una semana, segmentada por tipo de conexión. Con esa tabla, la elección entre optional, fallback y swap deja de ser una preferencia y pasa a ser una decisión con datos.