Previously: Browser Grading, Historical Browser Detection, Requirements and Loaders, Detection and Fallback, The Complete Solution.
We have a good quality solution, and I recommend that you stick with it. But what if we went a little crazy?
Madness
Our solution has a problem: it uses document.write(). And if you’re using Lighthouse to evaluate your page quality, this will ding you. (I think this is overblown. The primary reason people want you to avoid doocument.write() is because of bad adtech snippets that synchronously load JavaScript, blocking parsing for the entire download time. That’s not what we’re doing here, but the advice is overtuned.) But sometimes your boss tells you to get rid of it so you get a clean lighthouse score.
So we have another alternative. We can load the CSS asynchronously, and block rendering ourselves.
Now if we just wrote a normal HTML page, We’d link the CSS in the head and the JavaScript at the end of the body, meaning that when the JavaScript loads, the DOM is completely built and the CSS has all been loaded. If we loaded our JavaScript asynchronously, we could schedule our activity for DOMContentLoaded or even for load if we needed them. But when we asynchronously load our CSS and our JavaScript, we’ll need some other event to rely on.
So we can change our loader to fire off the events that we want!
var target = document.getElementsByTagName("script")[0];
var head = target.parentNode;
function hideDocumentUntilLoaded() {
function bootstrap() {
document.readyforscripting = true;
var e = new CustomEvent("readyforscripting");
document.dispatchEvent(e);
}
var hide = document.createElement("style");
hide.innerText = "body {display:none;}";
head.insertBefore(hide, target);
document.addEventListener(
"stylesloaded",
function () {
head.removeChild(hide);
if (document.readyState != "loading") {
bootstrap();
} else {
document.addEventListener("DOMContentLoaded", bootstrap, false);
}
},
false
);
}
var waiting = {};
var complete = {};
var handler = {
handleEvent: function (event) {
complete[event.target.href] = event.type === "load";
delete waiting[event.target.href];
var size = 0;
for (var key in waiting) {
if (Object.hasOwnProperty.call(waiting, key)) {
size++;
}
}
if (size === 0) {
try {
var e = new CustomEvent("stylesloaded", { detail: complete });
document.dispatchEvent(e);
} catch (e) {
if (
e.name === "TypeError" &&
e.message === "Object doesn't support this action"
) {
// swallow error from IE 9-11 CustomEvent is not new-able
} else {
throw e;
}
}
}
},
};
function insertStyle(src) {
var ss = document.createElement("link");
ss.setAttribute("rel", "stylesheet");
ss.setAttribute("href", src);
if (ss.addEventListener) {
ss.addEventListener("load", handler, false);
ss.addEventListener("error", handler, false);
waiting[ss.href] = true;
}
head.insertBefore(ss, target);
}
insertStyle("main.css");
insertStyle("more.css");
insertStyle("evenmore.css");
if (document.addEventListener) hideDocumentUntilLoaded();
As before, we hide the document with a style tag which we remove once the real CSS is loaded. But now we track which link tags are in progress and only remove it once they’re all done. (We use handleEvent here so we can process on both successful load and error.) When all the styles have loaded or failed, we fire a stylesloaded event.
When both stylesloaded and DomContentLoaded have fired, we fire readyforscripting and set document.readyforscripting, which we can use in our main script files to only start doing DOM manipulation when we’re actually ready. IE doesn’t let us make custom events, so those events don’t trigger in IE. (Not that you should be loading anything in IE anyway.)