encontré una fuente de 20 kb y publiqué igualmente la de 2 mb

dev · Oct 4, 2026 · 4 min read

mi sitio declara un rango de fuentes de U+0000-00FF — latino básico, los primeros 256 puntos de código. en la práctica eso significa acentos, puntuación y poco más. y sin embargo el archivo que había detrás pesaba 1.945.520 bytes.

en la misma carpeta había un segundo archivo, GoogleSansFlex-Latin.woff2, de 20.076 bytes. mismo tipo de letra. mismo diseñador. el 1% del tamaño.

eso no es un error de redondeo. ese archivo era el 0,99% del tamaño del que se descargaba, así que el 99% del peso de mi fuente de texto estaba ahí sin usarse.

la parte fácil

cambiar el nombre de archivo en la regla @font-face es una edición de diez segundos. la página sigue funcionando, el texto se sigue renderizando, y si nunca vuelves a mirarlo has ahorrado 1,9 mb de ancho de banda a cada visitante, para siempre, en cada página.

este es el punto en el que una persona razonable se detiene y publica.

así que primero medí

antes de tocar nada escribí una pequeña sonda de glifos y la ejecuté en un navegador de verdad, porque la diferencia de tamaño es obvia pero la pregunta útil es si el archivo pequeño cubre realmente los caracteres que usa el sitio. puedes hacerlo sin ninguna biblioteca:

const load = (url) => new Promise((resolve) => {
  const f = new FontFace("Probe", `url(${url})`, { weight: "100 1000" });
  f.load().then(() => resolve("ok")).catch((e) => resolve("fail " + e.message));
  document.fonts.add(f);
});

await load("/fonts/GoogleSansFlex-Latin.woff2");
await document.fonts.ready;

const c = document.createElement("canvas").getContext("2d");
const width = (family, ch) => {
  c.font = `50px "${family}"`;
  return c.measureText(ch).width;
};
// un carácter que la fuente no tiene cae en el respaldo, así que mide como
// la caja notdef: compara contra U+FFFF, a la que no mapea nada
const base = width("Probe", "\uFFFF");
const missing = [...text].filter((ch) => width("Probe", ch) === base);

el truco está en la línea base U+FFFF. el glifo notdef tiene un ancho real, y cualquier carácter que la fuente no tiene se resuelve a él, así que una coincidencia exacta con ese ancho es tu prueba de glifos faltantes.

lo ejecuté sobre todo el rango que me importa — mayúsculas y minúsculas, dígitos, puntuación, y cada letra acentuada que este sitio necesita para verse bien en italiano: àèéìòùÀÈÉÌÒÙäöüßñç, más €£¥©®™°·….

resultado: cero glifos faltantes. el subconjunto de 20 kb cubría todo, incluidos caracteres muy fuera del rango declarado.

luego comprobé la cosa real en vez de fiarme de la sonda:

c.font = '50px "Google Sans Flex Local", monospace';
// 264.7 px — no los 300 px que produce el respaldo, así que la fuente real está activa
const rendered = width('"Google Sans Flex Local", monospace', "Handgloves");
const fallback = width("monospace", "Handgloves");

los dos difieren, así que el subconjunto estaba realmente activo y no recurría en silencio al respaldo. según todas las medidas de las que dispongo, el cambio era correcto.

y luego lo revertí

porque los números no eran todo el problema. dos cosas que la medición no podía ver:

  • las formas no son idénticas. ambos archivos están recortados de la misma familia, pero los contornos del subconjunto no son los de la fuente variable con los valores por defecto de los ejes. los títulos de mi página son el texto más grande del sitio, y se veían visiblemente más pequeños y más ligeros que aquello contra lo que había diseñado.
  • los ejes variables habían desaparecido. el archivo que publiqué es una fuente variable, y el sitio establece wght y wdth mediante font-variation-settings. un subconjunto recortado para el latino no lleva necesariamente esos ejes. perder wght significa que cada font-black de la hoja de estilos recurre en silencio al peso statico que toque.

una regresión de tamaño en el título del hero no es algo que un contador de bytes te vaya a señalar. la vi, no me gustó, y volví a poner el archivo de 1,9 mb.

lo que realmente me quedé

el número que marcó la diferencia no es el tamaño. es la precarga:

<link rel="preload" href="/fonts/GoogleSansFlex-Variable.woff2"
      as="font" type="font/woff2" crossorigin />

el sitio es basado en texto, así que esa fuente está en el camino crítico. precargarla inicia la descarga durante el análisis del html en lugar de esperar a que el css llegue y se analice, y eso quita una ida y vuelta completa del primer pintado.

crossorigin es la parte que se olvida. las fuentes siempre se piden en modo cors, incluso same-origin, así que una precarga sin ese atributo la descarta el navegador y el archivo se descarga una segunda vez igualmente. obtienes lo peor de ambos: el coste de la precarga y ningún beneficio.

la conclusión honesta

1,9 mb es demasiado para una fuente de texto y sé cómo dejarlo en 20 kb. la corrección correcta no es elegir a mano un subconjunto latino — es construir el subconjunto yo con pyftsubset, manteniendo los rangos de ejes que la hoja de estilos usa de verdad, y comprobar el resultado renderizado contra el original antes de publicar.

hasta que eso exista, el archivo de 2 mb se queda y el sitio es honesto al respecto.

la parte útil de este ejercicio no fue el cambio. fue darme cuenta de que los dos archivos llevaban todo ese tiempo en la misma carpeta, y de que no tenía ni idea de cuál estaba descargando el navegador. ahora compruebo transferSize en una recarga, que da 0 para cada recurso que el navegador ya tiene en caché. ese número es el único informe de rendimiento honesto que he usado nunca.