Waiting for web fonts to load
While working on a project where I was required to sample dimensions of HTML elements with specific web fonts, I have bumped into a problem. There was no DOM event signaling when particular fonts were loaded and rendered. And even though all the fonts were base64 encoded and encoded into the web page. The browser didn't render them immediately. There was a slight delay between the first-page render and fonts rendering.
After a short research, I have found a hint in typekit’s blog. The solution presented in that post was to create an off-screen element, one for each font, with a bit of text and appropriate font-family value. Then periodically compare that element width to a reference width. Once element width changes, we can say that font is loaded and rendered.
And because I could not use 3rd party dependencies, I wrote a short onFontsLoad
function that implements a similar solution. This function receives an array of strings representing required font families and a callback function that is invoked when all passed fonts are loaded and rendered:
var context = window; | |
(function() { | |
var origWidth = null, | |
origHeight = null, | |
testDiv = null; | |
/** | |
* This function waits until all specified font-families loaded and rendered and then executes the callback function. | |
* It doesn't add font-families to the document, all font-families should be added to the document elsewhere. | |
* If after specific threshold time fonts won't be loaded, the callback function will be invoked with an error. | |
* | |
* The callback function is invoked with a single error parameter. | |
* If all fonts were loaded then this parameter will be null. Otherwise, object with message in the "message" field | |
* and array in "notLoadedFontFamilies" field with all not loaded font-families will be returned. | |
* | |
* @param {Array} fontFamiliesArray Array of font-families to load | |
* @param {Function} fontsLoadedCallback Callback function to call after all font-families loaded | |
* @param {Object} [options] Optional object with "maxNumOfTries" and "tryIntervalMs" properties. | |
* @return {Object} | |
*/ | |
this.onFontsLoad = function onFontsLoad(fontFamiliesArray, fontsLoadedCallback, options) { | |
var testContainer, clonedDiv, | |
notLoadedFontFamilies = [], | |
referenceFontFamily = "serif", | |
i, interval, | |
callbackParameter, | |
tryCount = 0, | |
maxNumOfTries = 10, | |
tryIntervalMs = 250; | |
function testDivDimensions() { | |
var i, testDiv; | |
for (i = testContainer.childNodes.length - 1; i >= 0; i--) { | |
testDiv = testContainer.childNodes[i]; | |
if (testDiv.offsetWidth !== origWidth || testDiv.offsetHeight !== origHeight) { | |
// Div's dimensions changed, this means its font loaded, remove it from testContainer div | |
testDiv.parentNode.removeChild(testDiv); | |
} | |
} | |
} | |
function finish() { | |
var testDiv; | |
window.clearInterval(interval); | |
testContainer.parentNode.removeChild(testContainer); | |
if (testContainer.childNodes.length !== 0) { | |
for (i = testContainer.childNodes.length - 1; i >= 0; i--) { | |
testDiv = testContainer.childNodes[i]; | |
notLoadedFontFamilies.push(testDiv._ff); | |
} | |
callbackParameter = { | |
message: "Not all fonts are loaded", | |
notLoadedFontFamilies: notLoadedFontFamilies | |
}; | |
} else { | |
callbackParameter = null; | |
} | |
fontsLoadedCallback(callbackParameter); | |
} | |
if (options !== undefined) { | |
if (options.maxNumOfTries) { | |
maxNumOfTries = options.maxNumOfTries; | |
} | |
if (options.tryIntervalMs) { | |
tryIntervalMs = options.tryIntervalMs; | |
} | |
} | |
// Use pretty big fonts "40px" so smallest difference between standard | |
// "serif" fonts and tested font-family will be noticable. | |
testContainer = document.createElement("div"); | |
testContainer.style.cssText = "position:absolute; left:-10000px; top:-10000px; font-family: " + referenceFontFamily + "; font-size:40px;"; | |
document.body.appendChild(testContainer); | |
if (testDiv === null) { | |
testDiv = document.createElement("div"); | |
testDiv.style.position = "absolute"; | |
testDiv.style.whiteSpace = "nowrap"; | |
testDiv.appendChild(document.createTextNode(" !\"\\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿŒœŠšŸƒˆ˜ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩαβγδεζηθικλμνξοπρςστυφχψωϑϒϖ–—‘’‚“”„†‡•…‰′″‹›‾⁄€ℑ℘ℜ™ℵ←↑→↓↔↵⇐⇑⇒⇓⇔∀∂∃∅∇∈∉∋∏∑−∗√∝∞∠∧∨∩∪∫∴∼≅≈≠≡≤≥⊂⊃⊄⊆⊇⊕⊗⊥⋅⌈⌉⌊⌋〈〉◊♠♣♥♦")); | |
// Get default dimensions | |
testContainer.appendChild(testDiv); | |
origWidth = testDiv.offsetWidth; | |
origHeight = testDiv.offsetHeight; | |
testDiv.parentNode.removeChild(testDiv); | |
} | |
// Add div for each font-family | |
for (i = 0; i < fontFamiliesArray.length; i++) { | |
clonedDiv = testDiv.cloneNode(true); | |
testContainer.appendChild(clonedDiv); | |
// Apply tested font-family | |
clonedDiv.style.fontFamily = fontFamiliesArray[i] + ", " + referenceFontFamily; | |
clonedDiv._ff = fontFamiliesArray[i]; | |
} | |
// Check if dimension of all divs changed immediately after applying font-family | |
// maybe all fonts were already loaded so we don't need to poll and wait. | |
testDivDimensions(); | |
// Check that there is at least one div, means at least one not loaded font. | |
if (testContainer.childNodes.length) { | |
// Poll div for their dimensions every tryIntervalMs. | |
interval = window.setInterval(function() { | |
// Loop through all divs and check if their dimensions changed. | |
testDivDimensions(); | |
// If no divs remained, then all fonts loaded. | |
// We also won't wait too much time, maybe some fonts are broken. | |
if (testContainer.childNodes.length === 0 || tryCount === maxNumOfTries) { | |
// All fonts are loaded OR (maxNumOfTries * tryIntervalMs) ms passed. | |
finish(); | |
} else { | |
tryCount++; | |
} | |
}, tryIntervalMs); | |
} else { | |
// All fonts are loaded | |
testContainer.parentNode.removeChild(testContainer); | |
fontsLoadedCallback(null); | |
} | |
}; | |
}).apply(context); |
The onFontsLoad
function also gets a third, optional parameter, which is an object with two fields:
maxNumOfTries
– maximum number tries to retrieve and compare the element dimension.tryIntervalMs
– interval in milliseconds between retrieving and comparing the element dimension.
The onFontsLoad
function iterates over the passed font-families array. For every font-family, it creates an element with a pangram text, and it applies a style with a matching font-family. It also creates one element with the "serif" font and stores its dimensions for reference. The pangram ensures that a change in size will occur even if the dimension of a single character of the requested font is different from the "serif" font. The font-size that is used on all the elements is 40px. This ensures that even a slight font appearance changes the element's size. Then, it polls the dimensions of the elements. When the dimensions of all the elements have been changed, the callback is invoked. However, the callback is invoked anyway if some fonts are still not loaded after a specific time (maxNumOfTries
* tryIntervalMs
).
The callback is executed with a single error
parameter. This parameter would be null
if all fonts were successfully loaded. Otherwise, this parameter will be an object with a message
property with an error message and additional notLoadedFontFamilies
property with an array of font-families that weren't loaded.
Here is a usage example: