I caratteri sottostanno a una regola di origine incrociata da cui immagini e script sono esenti. Servine uno da un altro nome host senza l'intestazione giusta e il browser lo scarica, si rifiuta di usarlo e ripiega — così il file risulta 200 nel pannello di rete, e la console lo spiega in un messaggio che nessuno legge.

Il messaggio

Access to font at 'https://cdn.example.com/f/inter.woff2' from origin
'https://yourdomain.com' has been blocked by CORS policy

L'intestazione

# nginx, on the host SERVING the font
location ~* \.(woff2?|ttf|otf|eot)$ {
    add_header Access-Control-Allow-Origin "https://yourdomain.com";
    add_header Cache-Control "public, max-age=31536000, immutable";
}
# apache / litespeed
<FilesMatch "\.(woff2?|ttf|otf|eot)$">
    Header set Access-Control-Allow-Origin "https://yourdomain.com"
</FilesMatch>

crossorigin sul precaricamento, se precarichi

<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
Senza crossorigin su quel tag il carattere viene scaricato DUE VOLTE: una dal precaricamento e una dal foglio di stile, perché sono considerate richieste diverse. La pagina risulta più lenta di quanto sarebbe stata senza alcun precaricamento.

La risposta più semplice

Servi i caratteri dal tuo dominio. Stessa origine significa: nessuna questione di CORS, nessuna risoluzione DNS in più, nessuna seconda stretta di mano TLS e un terzo in meno nella pagina. Con HTTP/2 non esiste più nemmeno un argomento di velocità a favore di una rete di distribuzione dei caratteri.

# download once, commit them, serve from /fonts/
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter.woff2") format("woff2");
  font-display: swap;
}
font-display: swap mostra subito il carattere di riserva e lo sostituisce quando arriva quello vero. Senza, su una connessione lenta il testo resta invisibile fino a tre secondi.