I have tried different approaches to avoid FOUT (Flash Of Unstyled Text) when I load a different Google Font via a user UI.
Initially I thought I could listen for the load event on the link element that I use to request the font from the Google API. However this link does not download the font itself – it merely downloads the @font-face that links to the actual font file.
Moreover, when your browser has downloaded the @font-face, it will apparently not download the actual font, it links to, until the font is actually used by a visible text element on the page. “Visible” means that the element must not be hidden with display: none.
Therefore, after some further digging, I decided to use the CSS Font Loading API as it should be designed exactly to take care of these sort of things. However, to my disappointment I still get a short FOUT. My code for loading Google fonts:
function requestGoogleFont(fontFamily, fontWeight, fontStyle, doOnLoad) {
let font = [fontStyle, fontWeight, '14px', fontFamily + ', ', 'sans-serif'].join(' '),
headElement = pageFrameContents.head,
fontVariant = fontStyle === 'italic' ? fontWeight + 'italic': fontWeight,
url = 'https://fonts.googleapis.com/css?family=' + fontFamily.split(' ').join('+') + ':' + fontVariant,
link = html.createAndAppend(headElement, '<link rel="stylesheet">'); // Adds a link element to the head element
fontLoadInitiator.style.font = font;
pageFrame.contentWindow.document.fonts.load(font)
.then(function(returned) {
doOnLoad();
});
link.setAttribute("href", url);
}
As you may have guessed from the code, the fonts are added to a document in an iframe. The callback, doOnLoad, simply adjusts the CSS to apply the chosen font. The fontLoadInitiator refers to a paragraph element that is on the page and styled invisible with opacity: 0. It has some text in it and its role is to get the browser to download the font as soon as the @font-face has been downloaded.
I am disappointed that I still get a short FOUT even after using the CSS Font Loading API. I can hide the flash by using a small timeout, but really I shouldn't have to since the callback should only run after the font is fully downloaded. I am out of ideas. I am hoping that some of you might have battled this beast in the past and have some useful experiences to share. Otherwise, my only option is the timeout (shivers!). I am using Google Chrome Version 93, by the way.
After some more experimenting, I found a solution - along with some insights about the CSS Font Loading API. Even though the API has been around for several years now, it is still experimental and it is clear that the documentation on MDM is still a bit lacking. Again, I have primarily experimented in Chrome.
What I found is that using promises does not work as expected. I have tried the load() function as well as the ready property. According to the documentation, FontFaceSet.load() forces the fonts to be downloaded. I thought it meant that the promise is not resolved until the loading is done (inspired by several blogposts about the subject). But it is actually very literal: it forces the fonts to be downloaded, which means that I don't have to style the fontLoadInitiator in order to provoke the download. However, it appears that the promise is resolved before the font is fully downloaded - hence the FOUT.
The FontFaceSet.ready property should according to the documentation contain a promise that resolves once font loading and layout operations have completed. In my experience, though, it does not. Rather, it behaves exactly as the promise returned by the load() function with the exception that it does not take fonts as an argument: It resolves before the font is fully downloaded and causes a FOUT and it also seems to force font download (must be of all pending fonts - since no specific font is passed) because it works without styling the fontLoadInitiator.
What worked for me was the FontFaceSet.onloadingdone event listener. This one does depend on pre-styling of the fontLoadInitiator. If the fontLoadInitiator is not pre-styled, the loadingdone event is never fired. Using this removes the FOUT.