Most projects treat web fonts as a nice extra. You pick a typeface, wire up some @font-face rules, and accept that everything renders in a fallback font for a moment before snapping into place. On a fast connection that’s tolerable. On a slow or flaky one, the “nice extra” sometimes never shows up at all.
HTTP caching and font-display help, but the browser still thinks in individual responses. I wanted something more direct: once a visitor has downloaded a font, keep it locally and reach for it, even when the network is slow or offline.
glyphcache.js is that idea packaged small. It stores font binaries in a persistent cache and injects @font-face rules that point at Blob URLs generated from the stored bytes. A warm cache means the browser renders with those fonts without touching the network again.
The Rules I Set First
The constraints came before any code. The API had to be boring enough to read at a glance. Browser-only, so no build system or framework requirements. And if anything failed along the way, the utility had to step aside quietly and let the browser do its normal font loading, worst case falling back to system fonts. I wrote that failure rule down before writing anything else, because “graceful fallback” is easy to say and easy to forget in the middle of an implementation.
Storing Fonts Where They Run
The HTTP cache is powerful but opaque. Headers, network conditions, and user settings all decide whether a response gets reused, and none of that is under my control. A service worker would give more control, at the price of more moving pieces and lifetime rules.
IndexedDB fit better. It’s built for binary data, it survives across sessions, and its API, while not exactly ergonomic, wraps up neatly in a few helpers for opening a database, reading by key, and writing values.
type DbOptions = { name: string; version: number };
function openStore({ name, version }: DbOptions): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const req = indexedDB.open(name, version);
req.onerror = () => reject(req.error);
req.onsuccess = () => resolve(req.result);
req.onupgradeneeded = () => {
if (!req.result.objectStoreNames.contains("fonts")) {
req.result.createObjectStore("fonts");
}
};
});
}
In the real utility, the font file URL is the key. Whatever versioning scheme a project already uses, hashed filenames or query params, feeds straight into cache invalidation. Change the URL and it becomes a new entry. Keep it the same and the cache serves what’s stored.
From Bytes to @font-face
With storage sorted, the rest is a short pipeline. For each font source, read the bytes from the cache. If they’re missing, fetch from the network and persist them. Wrap the bytes in a Blob, create a Blob URL, and generate a @font-face rule that mirrors what you would have written by hand.
I kept that logic in one place instead of burying it in call sites:
async function ensureFontsCached(fonts: FontDefinition[]): Promise<void> {
const db = await openStore({ name: "fonts-db", version: 1 });
// load, fetch if needed, and inject CSS for each font
}
The caller sees a single operation that says “warm up fonts,” while caching and CSS injection stay behind it. And from the browser’s point of view, once the @font-face block is injected, it’s just CSS. The browser doesn’t care whether the bytes came from the network or from IndexedDB. Design tokens, CSS variables, and components keep using the same font-family names they already know.
If IndexedDB is unavailable, a fetch errors, or a write throws, the utility calls the provided error handler and gets out of the way. The browser falls back to normal behavior.
Where I Drew the Line
glyphcache is scoped to the browser on purpose. It assumes document APIs exist and doesn’t try to be clever about Node, SSR, or asset pipelines. Those concerns belong to the build and deployment setup, not to a font cache.
I also skipped aggressive lifetime management for Blob URLs. Most apps use a small, stable set of fonts, so keeping a handful of Blob URLs alive for the life of the page is a fair trade for simpler behavior. If you’re at the point where font Blob lifecycle tuning is your main problem, things are going impressively well.
The public surface is deliberately tiny. Define a list of fonts that matches your design system, call the utility once on startup, forget about it. New visitors load fonts and warm the cache. Returning visitors and offline sessions get what’s already stored.
You could reproduce all of this by hand with a network tab, a Unicode‑length patience threshold, and some IndexedDB calls in the console. glyphcache just does it once, properly.