Alles was ihr nie über CORS und Font-Preloads wissen wolltet.

Futurama Blernsball Spieler erwartet einen Ball zugeworfen zu bekommen, kriegt aber stattdessen eine Torte ins Gesicht. Bildunterschrift: PRELOAD FONTS OHNE CROSSORIGIN ATTRIBUT

CORS und Font-Preloads – ein Zusammenspiel, das oft für Kopfzerbrechen sorgt. Wenn wir Schriften mit <link rel="preload" as="font"> vorladen, brauchen wir immer auch das crossorigin-Attribut. Wenn wir es vergessen, kann es zu unerwarteten Problemen führen, wie wir später sehen. Doch selbst wenn wir dieser Regel folgen, gibt es Fallstricke. Doch der Reihe nach:

  1. Was ist CORS?
    1. Das crossorigin-Attribut
    2. Safaris Sonderstellung
  2. Das vary: Origin-Dilemma
    1. Tragweite
    2. Ein Fallbeispiel
    3. Live-Demo des Problems
  3. Was Browser besser machen können

Was ist CORS?

Vereinfacht gesagt, ist es ein Protokoll, welches das Teilen von Ressourcen unterschiedlichen Ursprungs (Domain, Port, oder Protokoll) regelt. Die Server, von dem Ressourcen geladen werden sollen, deklarieren in HTTP-Headern, von welchen Ursprüngen (Webseiten) diese Ressourcen geladen werden dürfen. Wer mehr über CORS und dessen Ursprung (pun intended) lesen will, den verweise ich auf eine hervorragende Erklärung von Jake Archibald.

Im Rahmen dieses Posts ist wichtig sich zu merken, dass laut CSS Spezifikation Font-Anfragen vom Browser immer mit CORS gemacht werden. Dies gilt übrigens auch für Font-Anfragen vom selben Ursprung (same-origin). Die Fonts werden dann mit der CORS same-origin-Policy geladen. Warum das so ist, ist nicht einfach zu beantworten. Aber es ist wie es ist.

Das crossorigin-Attribut

Preloads werden nur mit CORS gemacht, wenn man das explizit verlangt. Dies tut man über das crossorigin-Attribut, denn die Existenz des Attributs macht die zugehörige Anfrage zu einer CORS-Anfrage und setzt gleichzeitig den zugehörigen Credentials-Modus. Da Font-Anfragen wie beschrieben laut Spezifikation immer mit CORS gemacht werden müssen und Preloads (aktuell) per default ohne CORS gemacht werden, brauchen wir das Attribut bei Font-Preloads.

Safaris Sonderstellung

Einzig Safari lädt Fonts nicht mit CORS und weicht damit von der Spezifikation ab, was für langanhaltende Diskussionen sorgt. Die Argumente gegen die Umsetzung sind für mich nicht ganz einfach nachzuvollziehen, aber Tatsache ist, dass Safari Fonts nicht im CORS Modus lädt. Probiert es aus.

Dieses Verhalten führt im Zusammenhang mit Font-Preloads zu ernsthaften Problemen, wie der nächste Abschnitt zeigt.

Das vary: Origin-Dilemma

Manchmal ist es nötig, dass Fonts mit einem vary: Origin-Header antworten. Etwa wenn das Laden eines Fonts von mehreren Ursprüngen erlaubt sein soll, nicht aber von allen.

Leider lässt sich ein Font-Preload in solchen Fällen nicht realisieren ohne Caching-Probleme zu verursachen. Man kann es nun einfach nicht mehr allen Browsern recht machen.

Fall 1: Preload mit CORS

Wenn wir uns an die Regeln halten und das crossorigin-Attribut setzen, bereitet Safari uns Probleme. Die Font-Anfrage initiiert vom Preload kommt nun mit einem Origin-Header. Die Anfrage initiiert vom CSS (ohne CORS) kommt ohne Origin-Header.

1. Überraschender Weise wird beim ersten Laden einer Seite mit diesem Setup, der Font aus dem Preload-Cache verwendet. Offenbar kann der vary: Origin-Header hier noch nicht berücksichtigt werden.

2. Wenn der Font aber beim zweiten Laden schon im HTTP/Memory-Cache liegt, kann der Header berücksichtigt werden. Der Font wird nun – initiiert vom CSS – erneut heruntergeladen.

Beim erneuten Laden der Seite wiederholt sich 1.. Der Font wird beim Preload erneut heruntergeladen, da es zu einem Origin-Mismatch kommt. Danach wird der Font aus dem Preload-Cache wiederverwendet. Beim nächsten Laden wiederholt sich 2., usw.. Bei jeden Laden wird der Font genau einmal vom Server heruntergeladen. Probiert die Demo weiter unten aus, um das Verhalten zu überprüfen.

Fall 2: Preload ohne CORS

Dieser Fall ist deutlich schwerwiegender, da er alle Chromium-Browser betrifft und die entsprechenden Fonts bei jedem Refresh der Seite doppelt(!) lädt.

Durch die Preload-Anfrage und die darauf folgende Anfrage aus dem CSS kommt es zu alternierenden Anfragen bzgl. des Origin-Headers. Jede Anfrage kann nicht auf den HTTP-Cache zurückgreifen und lädt den Font erneut vom Server herunter und speichert die Antwort im Cache. Das Caching wird so komplett ausgehebelt. Bei jedem Seitenaufruf wird der Font doppelt heruntergeladen.

Tragweite des Problems

Folgende Zahlen konnte ich ermitteln:

  • 11% aller Seiten benutzen Font-Preloads für mindestens einen Font.
  • Eine Abfrage in der httparchive BigQuery DB zeigt, dass etwa eine in 30 Font-Anfragen (3,38%) mit einem vary: Origin-Header antworten.
  • 98,5% aller <link rel="preload" as="font">-Tags haben das crossorigin-Attribut.
  • Alle Zahlen zusammengeworfen komme ich damit etwa darauf, dass Fall 1 auf etwa einer in 273 Webseiten auftritt. Fall 2 kommt glücklicher Weise deutlich seltener vor (eine in 17.930 Webseiten). Das mag nach wenig klingen, aber auch große Plattformen können betroffen sein – wie das nächste Kapitel zeigt.

    Ein Fallbeispiel

    Vor kurzem bin ich über ein Problem auf der Bluesky Web-App gestolpert als ich ein spannendes Tool von Brian Louis Ramirez ausprobierte, welches im Rahmen des Web Performance Calendars vorgestellt wurde.

    Es stellte sich heraus, dass Fonts ohne crossorigin-Attribut vorgeladen wurden und die Antwort der Anfragen einen vary: Origin-Header enthielt. Es kam also genau zum oben beschriebenen Szenario. In diesem konkreten Fall bedeutete das, dass mit jedem Refresh über 1MB extra an Daten vom Server zu Besuchenden transferiert wurden. Und das alles wegen eines nicht gesetzten Attributs. Eins, das zudem noch kontraintuitiv hätte gesetzt werden müssen, da die Fonts vom gleichen Ursprung geladen wurden.

    Die Folgen des vergessen Attributs

    Zusätzlich zur verschlechterten Performance beim Laden der Fonts hatte der Fehler auch Auswirkungen auf die CO2-Bilanz der Webseite.

    • Den Fehler gab es etwa drei Monate von Oktober bis Dezember. In diesem Zeitraum hatte die Webseite 350 Million Besuchende laut similarweb, davon waren schätzungsweise 77% (also 270 Million) mit Chromium-Browsern unterwegs.
    • Wenn wir konservativ rechnen und davon ausgehen, dass pro Besuch kein Refresh stattfindet, und 10% erstmalig Besuchende waren, dann wurden durch diesen Fehler insgesamt 256 Terabyte an Daten nutzlos zu den Besuchenden gesendet. Bei Safari Besuchenden wurden ironischer Weise 28 Terabyte durch den Fehler eingespart (minus mal minus 😊).
    • Mit der Open-Source-Bibliothek co2.js kommt man mit diesen Zahlen auf 31 Tonnen emittiertes CO2e zusätzlich.

    Demo des Problems.

    Ich habe hier eine kleine Live-Demo vorbereitet. Probiert es mit verschiedenen Browsern aus. Beim Klick auf einen der Buttons ladet ihr einen iframe neu. Dieser enthält einen Webfont, der per Preload geladen wird und vom CSS verwendet werden will. Der iframe enthält Infos über alle HTTP-Requests zu diesem Font. Der Text auf den Buttons bedeutet dabei:

    • mit/ohne CORS: Preload des Fonts wird mit/ohne crossorigin-Attribut
    • mit/ohne vary: der geladene Font antwortet mit/ohne vary: Origin-Header

    Interessant finde ich insbesondere, dass in Chromium-Browsern bei Antworten ohne vary: Origin-Header der HTTP-Cache nach dem Preload ohne CORS verwendet werden kann – nicht aber der Preload-Cache.

    Dass Firefox Font-Preloads ohne crossorigin-Attribut trotzdem im CORS Modus macht, war eine weitere schöne Überraschung.

    Was können Browser tun? Macht es wie Firefox!

    Browser sollten bei Preloads per default den Modus des angefragten Datei-Typens übernehmen. Das bedeutete in Chromium, dass Preloads von Fonts automatisch mit CORS geschehen. Ein entsprechender Vorschlag für die HTML Spezifikationen wurde bereits eingereicht. Die Umsetzung würde die potentiellen Probleme bei Chromium beseitigen. Ich hoffe sehr, dass der Vorschlag dieses Mal angenommen wird. Vor 10 Jahren wurde eine ähnlicher Vorschlag noch abgewiesen.

    Bei Safari würde die Änderung allerdings keinen Einfluss haben, da hier ein explizit gesetztes crossorigin-Attribut ignoriert werden müsste. Ich glaube nicht, dass das im Sinne des Vorschlags ist. Stattdessen sollte Safari Font-Anfragen endlich mit CORS tätigen. Damit würden sie das beschriebene Problem umgehen, der Spezifikation folgen und für konsistentes Verhalten in allen Browsern sorgen.

    Kurzum: Chromium und Safari sollten sich so verhalten wie es Firefox tut. Font-Anfragen werden mit CORS getätigt und Preloads von Fonts führen unabhängig vom crossorigin-Attribut zu einem CORS Request. Probiert es oben mit Firefox aus.

    • Die Frage, warum dass crossorigin-Attribut bei same-origin-Font-Preloads notwendig ist, wurde u.a. auf Stackoverflow gestellt und hat eine sehr gute, detaillierte Antwort erhalten. Wie es allerdings überhaupt dazu kam, ist schwer nachzuvollziehen. Wenn ich die W3C-Diskussionen richtig verstehe, so handelt es sich eher um eine Implementierungsentscheidung als um eine bewusste Sicherheitsmaßnahme. Arrow Up Right
    • Ein Argument gegen das Umsetzen ist, dass Font-Dienste das CORS Feature eher als DRM-Werkzeug einsetzen wollten, was nicht der ursprünglichen Intention des CORS-Modus entspricht. Arrow Up Right
    • Browser benutzen maximal einen Platz pro URI im HTTP-Cache. Der vary-Header wird nur zur Validierung verwendet: stimmen die vary-Werte der im Cache gespeicherten und der angefragten Ressource nicht überein, wird der Cache für diese URI neu überschrieben. Das Caching-Verhalten scheint diesbezüglich übrigens nicht einfach verbessert werden zu können und ein entsprechender Fix-Request für den vary: Accept-Header in Chromium wurde abgewiesen. Arrow Up Right
    • Das Tool generiert eine Tonfolge anhand der Anfragen, die durch einen Seitenaufruf entstehen. Jede Anfrage erzeugt einen Ton, der solange anhält, wie der entsprechende Download dauert. Beim Refresh der Bluesky Seite zeigte sich, dass einige Anfragen deutlich länger dauerten als alle anderen. Arrow Up Right
    • Beim erstmaligen Besuch wurden bei Chromium 505KB zusätzlich versendet, beim wiederholten Besuch 1,01MB. Bei Safari wurden Daten eingespart: 505KB bei jedem wiederholten Besuch. Arrow Up Right
    • Berechnung mit der co2.js (v0.16) Bibliotheksmethode perVisitTrace(1010*1000, false, {dataReloadRatio: 0.5, firstVisitPercentage: 0.9, returnVisitPercentage: 0.1}); ergibt das 0.116g pro Seitenaufruf. Mulitpliziert mit den 270 Million Chromium Besuchenden sind das 31,32 Tonnen. Mit analoger Rechnung kommt man bei Safari auf 3,3 Tonnen Einsparung. Arrow Up Right
    • Die Abfrage aus dem HTTP-Cache klappt allerdings nur, wenn die Antwort des no-CORS Requests mit Access-Control-Allow-Origin-Header kommt. Arrow Up Right
    • Tatsächlich wurde das Thema schon beim Verfassen der Spezifikation für Preload-Links vor knapp 10 Jahren in einem Github-Issue diskutiert. Es ist nicht ganz einfach der Diskussion zu folgen. Aber das Feedback zur Frage, ob man das Attribut implizit setzen sollte, lautete mehrstimmig: nein. Die einzige Begründung, die ich der Diskussion entnehmen kann, ist, dass eine Änderung eines Aufrufs von no-CORS auf CORS zu “magisch” sei. Arrow Up Right
    Tags #webperf

    Weitere Posts