DEV Community

NullPointerZen
NullPointerZen

Posted on

Two canvas font shortcuts worked in three browsers and drew the fallback in Firefox


You build a share card on a canvas: a headline in a nice serif, a price, a button that saves the whole thing as a PNG. The first time the page opens, the saved image has the headline in plain sans-serif. Reload and it looks right, and the console is empty both times. I make small share-image generators for a content team and wanted to know which way of "waiting for the font" I could lean on, so I wrote a test page to compare them.

The plain version first. @font-face is the CSS rule that gives a font a name and says where its file lives. Declaring it doesn't download the file, and canvas fillText doesn't wait for it. On my page, drawing straight after the declaration came out in the fallback, the generic font listed after mine in the font string, with no error. That happened in all four browsers I tried, Chromium 149, Chrome 155, Firefox 151 and WebKit 26.5, on one Apple M4 running macOS.

So I have to wait for something, and I had three candidates. The first two are shortcuts that look like waiting. One is to put some ordinary page text in the font and draw in the load event, which fires once the page and its resources have finished loading. The other is a preload, <link rel="preload" as="font" crossorigin>, which asks the browser to fetch the file early, and then drawing as soon as that link reports it is done. Neither one asks about the font itself. It is like deciding the kettle has boiled because the toast popped. The third asks directly: document.fonts.load() takes a font string and returns a promise that resolves when that font has been loaded.

Nine ways to draw on a canvas and which font showed up in Chromium 149, Chrome 155, Firefox 151 and WebKit 26.5

Every cell was compared pixel by pixel with two reference renders, one drawn with the font fully loaded and one with the fallback alone. The rows to look at are the fifth, sixth and eighth. Both shortcuts are green in three columns and red under Firefox 151, and the document.fonts.load row is green all the way across. The test font is a 3,755-character subset of Noto Serif SC, 1,046,216 bytes as WOFF2. Noto Serif SC and Pacifico, the Latin font mentioned below, are both SIL OFL 1.1.

Firefox was the odd one out both times. With the preload, drawing the moment the download finished gave the fallback there, and drawing again 1,000 ms later gave the custom font. I didn't look into why. The second row of the chart bothered me more. The load shortcut only worked anywhere because some page text used the font. When I removed that text, drawing in load gave the fallback in all four browsers, and the custom font appeared when I drew again 1,500 ms later. document.fonts.ready, a promise for the page's fonts as a whole, behaved the same way. With the text on the page it was fine in all four. Without it the promise came back in under 20 ms and I got the fallback in all four. A wait that only holds while some unrelated text stays on the page is one I don't trust myself to keep intact.

Awaiting document.fonts.load gave the custom font in all four browsers, with the Chinese subset, with Pacifico, and with the full 11.6 MB Noto Serif SC file. The wait does cost time. On my local server throttled to a fixed 20 Mbps, a rate I set myself and not a real network, the same subset took about 0.5 seconds to come back. I also tried document.fonts.check(), the call that answers whether a font is ready. It returned true for a font name I had never declared, in all four browsers, so I don't use it as a gate.

To see how a finished product deals with this, I used ImgIng (https://imging.ai/), whose online design editor has text layers with downloadable fonts and says under the font menu how big the first download is. I delayed the Korean font's response on purpose, picked Noto Sans Korean and clicked export about 0.2 seconds later. The file was written only after the font response arrived, and it was byte-for-byte identical to the PNG I exported after waiting, 39,765 bytes, against 39,854 for the system-font export.

The online design editor while the Korean font is downloading and after it has arrived

In the top half the status bar says the text layer font is still downloading and the layer is 648 wide. In the bottom half the glyphs have changed and the width reads 677. I only watched this one flow, in Chromium 149 with one font, and I haven't seen how it is written inside.

My share-card pages now await document.fonts.load with the exact font string right before the first draw. How any of this behaves on phones, on Windows, in release Safari or in Edge I can't say, because I tested none of them.

Top comments (0)