CORS and font preloads – it’s complicated. When fonts are preloaded via <link rel="preload" as="font">
, we must always include the crossorigin
attribute. If we forget it, unexpected issues may arise as we will see later. But there are scenarios where problems cannot be avoided – even if best practices are followed. But let’s start at the beginning:
What is CORS?
In simple terms, CORS is a protocol that lets webservers control which resources are allowed to be downloaded from other origins (different domain, port or protocol). The webserver declares in HTTP headers which origins are allowed to download given resources. If you want to know more about CORS and its origin (pun intended), check out this great article be Jake Archibald
In the context of this post it’s important to understand that – according to CSS specification – font requests shall always be made with CORS. This also applies to when the requested font is hosted on the same origin (same-origin
). In such cases it adheres to the CORS same-origin
policy. Why this is the case is not easily explained. But it is what it is.
Also important to know: CORS requests will come with an Origin
HTTP header, where the value is set to the origin that initiates the request. This header can be used to determine if the request is allowed or not.
The crossorigin
Attribute
Requests initiated by preloads will only be made with CORS if we explicitly demand so. This is done via the crossorigin
attribute, which will classify the request as CORS and also sets the associated credentials mode. Since, as noted, font requests must always be made with CORS and while preloads are (currently) performed without CORS by default, the crossorigin
attribute is required for font preloads.
Safari’s Peculiar Behavior
Only Safari deviates from the specs and does not load fonts with CORS when initiated from CSS. This has resulted in ongoing discussions. The arguments against following the specs are a bit hard to grasp, but again, it is what it is. Try it yourself.
While this deviation does not cause harm in most circumstances, it can lead to severe issues when combined with font preloads.
The vary: Origin
Dilemma
Sometimes it’s necessary to include a vary: Origin
header in the response to a font request. For instance, when we want to allow loading of a font from more than one origin, but not all origins.
Unfortunately, we are now in a scenario where we can’t preload fonts without causing caching issues. There is currently no way to satisfy all browsers.
If we follow best practice and set the crossorigin
attribute, Safari causes problems. The font request initiated by the preload now comes with CORS and hence the Origin
header. The request initiated by CSS will not be CORS and will not have the Origin
header. Now the browser behaves as follows:
Step 1: Surprisingly, on initial page loads the preloaded font will be served from the preload-cache, when it’s needed in CSS. It seems that the vary: Origin
header can’t be considered because the response doesn’t arrive in time.
Step 2. On reload the font is in the HTTP Cache. When CSS needs it, it now knows that the Origin
doesn’t match. It downloads the font again.
On the next reload step 1 kicks in again. The preload causes another download because of an Origin
mismatch with the HTTP cache entry. We are in a cycle where step 1 and 2 are repeating endlessly. Every page reload causes another font download.
This case is even worse because it affects all Chromium-based browsers and it causes the affected fonts to be downloaded twice on every page reload.
The preload and the following request from CSS result in alternating requests regarding the Origin
header. Each request cannot serve the file from the HTTP cache because of the Origin
mismatch. Instead it downloads the font anew and saves the response in the HTTP cache. This download happens twice per affected font on every page load.
The Scope of the Issue
Here are some numbers to quantify the impact:
- 11% of all websites use font preloads for at least one font.
- An HTTPArchive BigQuery analysis shows that 3.38% of font requests include a
vary: Origin
response header. - 98.5% of
<link rel="preload" as="font">
elements specify acrossorigin
attribute.
Combining these statistics, we can estimate that Case 1 (CORS preload with vary: Origin
) affects roughly 1 in 273 websites, while Case 2 (non-CORS preload with vary: Origin
) is less common, affecting 1 in 17,930 websites. That might seem insignificant, but major platforms can still be impacted — as demonstrated in the next section.
A Real-World Example
Recently, I came across this exact issue while testing the Bluesky web app. The problem became apparent when I used a fun tool by Brian Louis Ramirez that was introduced in the Web Performance Calendar.
It turned out that Bluesky was preloading their webfonts without CORS and those fonts answered with a vary: Origin
header. The scenario described above appeared and in this specific case caused more than 1MB of data uselessly being transferred on every page load, including refreshes. Purely because of a missing attribute, one that is called crossorigin
and counter-intuitively should have been set for same-origin
fonts.
The consequences of a Missing Attribute
In addition to the decreased font-loading performance the mistake had a substantial impact on carbon emissions.
- The issue was present for around three month (Oktober – Dezember 2024). In this period the website das 350 million visits according to similarweb.
- Roughly 77% of visitors were using Chromium-based browsers 18% were using Safari.
- Calculating conservatively, we assume that no page refreshes were done during visits and that 10% of those visits came from first-time visitors.
- On Chromium the bug caused 505KB of extra data transfer for first-time visitors, for repeated visitors the bug caused 1.01MB of extra data transfer. Ironically, on Safari the bug saved data transfer. For repeated visitors it saved 505KB of extra data transfer.
- The numbers above lead to 256 Terabytes of unnecessary data transfer on Chromium-based browsers and 30 Terabytes of saved data transfer on Safari.
- With the open-source library co2.js this leads to 28 tonnes of CO2e being emitted uselessly.
Live Demo of the Problem
I prepared a small live demo. Check it out with different browsers. On click on one of the buttons below we can reload an iframe
. It preloads a webfont that CSS then tries to use. Info about the HTTP-request(s) is then output in the iframe
. The text on the buttons have the following meaning:
- with/noCORS: Fonts preload will be done with/without
crossorigin
attribute - with/no vary: the font responds with/without
vary: Origin
header
What I find particularly interesting is that in Chromium-based browsers the font preloaded without CORS will be fetched from the HTTP cache if no vary: Origin
is involved.
Another nice surprise was to see that Firefox preloads fonts with CORS even when no crossorigin
attribut is set.
How Browsers Can Improve? Be like Firefox!
Browsers should default to the mode of the requested resource type when doing preloads. In Chromium-based browsers this means, that font preloads should automatically be done with CORS. An according proposal has been submitted for the HTML specifications. Implementing this suggestion would eliminate potential problems in Chromium. I really hope the proposal will be accepted this time around – unlike 10 years ago when a similar one was rejected.
For Safari, however, this change would not eliminate the issue, because in Safari’s case the crossorigin
attribute that is explicitly set would have to be ignored. I don’t think that this would be intended in the proposal mentioned above. Instead, Safari should make font requests with CORS like all the other browsers do. Together with the proposal above this would eliminate the described issues, it would follow the specification and ensure consistency between browsers.
In short: Chromium and Safari should follow Firefox’s example. Firefox correctly initiates font requests with CORS and executes font preloads as CORS requests, regardless of the presence of a crossorigin
attribute. Try it yourself using the live demo above.
-
The question of why the
crossorigin
attribute is necessary forsame-origin
font preloads has been addressed in detail on Stack Overflow. . However, tracing back why this requirement was made in the first place is challenging. If I understand W3C discussions correctly, it was more an implementation decision than a deliberate security measure. - One argument is that font services have used the CORS mechanism as a form of DRM, which conflicts with the original purpose of CORS.
-
Browsers allocate only a single HTTP cache entry per URI. The
vary
header is only used for validation: if thevary
values of the cached resource and the requested resource do not match, the cache entry for that URI is overwritten. The caching behavior is difficult to improve, and a proposal to fix the handling ofvary: Accept
in Chromium was rejected. - The tool generates a sound trace of all requests that occur during a page load. Each request will make a beeping sound that lasts as long as the loading of its response takes. On a refresh of https://bsky.app I heard that a few requests took much longer than all others.
-
Calculation with the co2.js (v0.16) library method
perVisitTrace(1010*1000, false, {dataReloadRatio: 0.5, firstVisitPercentage: 0.9, returnVisitPercentage: 0.1});
this results in 0.116g per page view. Multiplied with the 270 Million Chromium-based visitors this makes 31.31 tonnes. Analogue calculations for Safari lead to 3.3 tonnes of savings. -
This only works if the response to the no-CORS preload contains the correct
Access-Control-Allow-Origin
header. -
This topic was already debated during the draft of the preload specification nearly a decade ago. The general feedback on the suggestion was a “no” to implicit
crossorigin
behavior. The only rationale I could identify in the discussion was that silently shifting from no-CORS to CORS seemed too “magical”.
- https://drafts.csswg.org/css-fonts/#font-fetching-requirements (last opened: 11.03.2025)
- https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#requests_with_credentials (last opened: 11.03.2025)
- https://github.com/whatwg/html/issues/10891 (last opened: 11.03.2025) https://github.com/whatwg/html/issues/10891 (last opened: 11.03.2025)
- https://bugs.webkit.org/show_bug.cgi?id=86817 (last opened: 11.03.2025)
- https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches (last opened: 11.03.2025)
- https://almanac.httparchive.org/en/2024/fonts#fig-15 (last opened: 11.03.2025)
- https://gist.github.com/ko-jordan/4adc2c7c57d149695b635018ba81f05e (last opened: 11.03.2025)
- https://bsky.app/profile/tunetheweb.com/post/3ld2lp3c2i22i (last opened: 11.03.2025)
- https://calendar.perfplanet.com/2024/ (last opened: 11.03.2025)
- https://www.similarweb.com/website/bsky.app/#ranking (last opened: 11.03.2025)
- https://gs.statcounter.com/ (last opened: 11.03.2025)
- https://stackoverflow.com/questions/70183153/why-do-we-need-the-crossorigin-attribute-when-preloading-font-files (last opened: 11.03.2025)
- https://github.com/w3c/preload/issues/32 (last opened: 11.03.2025) https://github.com/w3c/preload/issues/32 (last opened: 11.03.2025)
- https://jakearchibald.com/2014/browser-cache-vary-broken/ (last opened: 11.03.2025)
- https://www.mnot.net/blog/2017/03/16/browser-caching (last opened: 11.03.2025)
- https://issues.chromium.org/issues/40876300 (last opened: 11.03.2025)
- https://bsky.app/profile/screenspan.net/post/3lcfir6csj22s (last opened: 11.03.2025)
- https://developers.thegreenwebfoundation.org/co2js/overview/ (last opened: 11.03.2025)