I want share cards on one of my small tool sites, drawn on a canvas: a title, a price line, one serif web font. The plan is to do that drawing in a Web Worker, a background thread that keeps the page responsive, using OffscreenCanvas, which is a canvas that does not have to sit in the DOM. Before writing the real thing I built a test page, and the first card from the Worker came back in the fallback font, meaning the generic family at the end of the font string, sans-serif in my case. No error was thrown. A moment earlier the same font had drawn fine on the main thread.
The test page ran on one machine, an Apple M4 on macOS, in Chromium 149, Chrome 155, Firefox 151 and WebKit 26.5 (the macOS engine build, not a Safari release). The font was Noto Serif SC cut down to 3,755 common characters, a 1,046,216-byte WOFF2, drawing a short Chinese sale line at 48px. To decide which font had been used I hashed the canvas pixels and compared them with two reference renders, one made after the font was known to be loaded and one made with plain sans-serif.
A font loaded on the page is not loaded in the Worker
On the main thread I created a FontFace (a font object built in script instead of in CSS), awaited its load(), added it to document.fonts and drew. That drew the custom font. Then I posted the same font string and the same text to a Worker and drew on an OffscreenCanvas there. All four browsers gave me the fallback.
The Worker has its own font list. It is called self.fonts, the counterpart of document.fonts on the page, and all four browsers had it along with the FontFace constructor. I now think of the Worker as a print shop across town: the font installed on my desk machine does nothing for their presses until I send them a copy.
Loading it in the Worker works when it happens before the first draw
So the Worker loads the font itself first. It builds its own FontFace from the font URL, awaits load(), calls self.fonts.add(), and only then creates the canvas and draws. With this order all four browsers drew the custom font on the first try.
Drawing first left the font string on the fallback
Then I tried the order I would have written without thinking: draw right away, load the font afterwards, redraw.
Firefox redrew with the custom font. Chromium, Chrome and WebKit stayed on the fallback. In Chromium and Chrome I waited 300 ms and drew again, then called self.fonts.load with that font string and drew again, and got the fallback both times. Next I created a brand new OffscreenCanvas inside the same Worker. WebKit switched to the custom font on the new canvas; Chromium and Chrome still did not.
Row two is the only one that is green everywhere. The two rows under it are the same Worker after one early draw.
One more thing I poked at and do not fully trust. After the string was stuck I changed 48px to 47px and measured the text. All four browsers reported a width of 388, which is what the custom font scales to; the fallback would have been about 391.7. I compared widths only, never pixels, so I am not shipping that as a fix. Why the original string stays stuck I did not look into.
So my plan now is that the Worker gets the font URLs in its first message and awaits every load before it creates a 2D context. If a placeholder is needed meanwhile, the page draws it, not the Worker.
To see how a shipped tool behaves when a font has not arrived yet, I used ImgIng (https://imging.ai/), a browser image tool whose online design editor has text layers with fonts that are fetched when you pick them. I delayed the Korean font's response, selected Noto Sans 한국어 and clicked export about 0.2 seconds later. The file was only written after the font response arrived, and it was byte-for-byte identical to an export I made after letting the font settle, 39,765 bytes each. The system-font export was a different file at 39,854 bytes. That is one flow with one font in desktop Chromium 149. I did not look at how it is built, so I cannot say whether a Worker is involved.
Both strips say 가을 신상품 세일. The top one is the system font. The bottom one was exported right after the font switch, and its strokes are visibly thinner.
The check I kept fits in any Worker: draw the text once with your font string and once with plain sans-serif, then compare the pixels. If they are identical, you are on the fallback. None of this was run on phones, Windows, a Safari release or Edge. The test font is Noto Serif SC, which is under SIL OFL 1.1.

Top comments (0)