The shared HTTP cache died in 2020

performancegdprweb

There is an argument I still see in code review, in meetings and in discussion threads: "load your fonts from fonts.google.com, that way if the visitor already downloaded them on another site, they already have them cached". The argument is elegant. It used to be true. It has not been true since October 2020, and it is worth understanding why, because the reasoning that killed it applies to a great deal more than typography.

What the shared cache promised

The original reasoning was sound. The browser HTTP cache was keyed by the resource URL, and by nothing else. A file downloaded from fonts.gstatic.com while visiting site A stayed available to site B, to site C, to any site at all. The more popular the CDN, the higher the odds of an already warm cache. At web scale, the pooling genuinely made sense.

The trouble is that this same pooling was a leak channel. If a site can measure that a resource is already cached, it can infer that you visited another site. Chain enough measurements together and you reconstruct a browsing history, and you manufacture a persistent identifier that survives the deletion of cookies. This is the family of attacks known as XS-Leaks, and supercookies.

The browsers settled it

Chrome partitioned its HTTP cache as of version 86, rolled out over late 2020. A resource is no longer keyed by its URL alone, but by a key that includes the top-level site requesting it. The same font downloaded on two different sites now occupies two distinct cache entries. The pooling disappeared by construction.

Firefox followed with version 85, on 26 January 2021, and went further still. Mozilla does not only partition the HTTP cache: the list covers the image cache, favicons, HSTS, OCSP, the stylesheet cache, DNS, HTTP authentication, Alt-Svc, TLS certificates, and, explicitly, the font cache. Connection pooling, preconnects and TLS session identifiers are partitioned as well. In other words, even the reused connection to fonts.gstatic.com no longer crosses the boundary between two sites.

Safari partitions too, by eTLD+1 of the top-level site, and did so before the others.

So the three engines that matter have all removed the exact property the argument rested on. This is not an announced deprecation, nor an experimental mode: it is the default behaviour of every one of your visitors' browsers, and has been for five years.

The number that should end the debate

The most interesting part is not that the shared cache disappeared. It is what its disappearance cost, as measured by Google on its own fleet:

The overall cache miss rate increases by about 3.6%, changes in FCP (First Contentful Paint) are modest (~0.3%), and the overall fraction of bytes loaded from the network increases by about 4%.

Three tenths of a percent of FCP. Google dismantled the cross-site cache of the entire web and paid 0.3% of render time for it. That says something retrospective and fairly brutal: even before partitioning, the real benefit of the pooling was marginal. We optimised for an effect nobody had measured, for years.

What is left, once the argument is withdrawn

Without a shared cache, you have to look at what a call to Google Fonts actually costs, and the balance flips.

A classic integration starts with a stylesheet on fonts.googleapis.com. That stylesheet then references the font files, hosted on fonts.gstatic.com. Two domains, so two DNS resolutions, two TLS handshakes, and above all a chain of requests: the browser cannot discover the font file URL before it has received and parsed the CSS. You have added a network round trip to the critical rendering path, for the benefit of a cache that no longer exists.

Self-hosting puts the fonts on the same domain as the document, over an already open connection, discoverable straight from the HTML, and preloadable. The critical path gets shorter.

And then there is the law

The technical argument is sufficient on its own, but it does not live alone. A request to a Google server transmits the visitor's IP address, which is personal data, to a third party, in the United States, without prior consent.

On 20 January 2022, the Landgericht München I (case 3 O 17493/20) ruled that this transmission was unlawful. A private individual was awarded 100 euros in damages, along with an injunction to stop. The amount is anecdotal. The precedent is not, and it opened a wave of cease-and-desist letters in Germany.

In France, there is to my knowledge no CNIL decision bearing specifically on Google Fonts. The applicable reasoning is the more general one coming out of the Schrems II ruling on transfers to the United States. And there is a simple test any DPO will put to you: if the same visual result is achievable without calling a third party, it becomes very hard to explain how the outsourcing was necessary.

What self-hosting costs

I am not going to pretend it is free. In bringing your fonts home, you inherit three responsibilities the CDN was carrying for you.

You become responsible for the character subset. Google automatically served a trimmed version suited to the browser. On your own, a complete font can weigh several hundred kilobytes where a Latin subset weighs a few dozen. That is a tool to run, once.

You become responsible for the cache headers. A font file is immutable: it deserves a versioned name and a Cache-Control: public, max-age=31536000, immutable. If you leave your server's default configuration in place, you will lose more than you gained.

You become responsible for updates. A self-hosted font does not receive upstream glyph fixes. In practice, for running text, this is a non-issue.

In exchange, you get a property the CDN cannot offer: reproducibility. On this site, the PDFs of my CV are generated at build time by a headless browser. With remote fonts, the rendering of those documents would depend on a third-party service being available at the precise moment of compilation, and two identical builds could produce two different files. With fonts versioned in the repository, the build is hermetic.

The real lesson

The shared HTTP cache is a textbook case of an optimisation turned folklore. The argument was correct, the context changed, and the argument kept circulating without its context. Five years on, we are still repeating it.

It is worth asking, of the other things our trade holds self-evident, which one has already been refuted by a release note nobody read.

Sources


← Back to blog