In Part One, we looked at the historical practice of browser grading. In Part Two, we looked at some historical solutions for browser detection. Today, we’re going to start putting it together.
Requirements
Let’s start by thinking about browser support. A lot of our historical solutions came from a world where ongoing support for IE was an issue; thank goodness we don’t live there anymore. Our biggest danger these days is someone coming to our site with an old phone that has a several years old version of webkit or chromium. Especially if they are using an app-hosted browser that uses a system webview.
There are 3 browser engines commonly in use, and 3 obsolete browser engines. These are our browser grades:
A-grade experience: If someone comes to the site with an up-to-date browser running Gecko, Blink, or Webkit, we want to give them the full experience.
Obsolete C-grade experience: If someone comes to the site with an out-of-date browser running Gecko, Blink, or Webkit, we want to give them the plain HTML experience.
Archaic C-grade experience: If someone comes to the site with a browser running Trident (IE,) Spartan (Old Edge,) Presto (old Opera,) or some other ancient thing (Netscape? iCab? Lynx?) we want to give them the plain HTML experience.
It’s probably worth the time to add a few more functional requirements:
Opt-In: We’ll have to develop a plain HTML experience for our site. It would be nice to be able to develop this in a modern browser, so we should be able to opt-in to the plain HTML experience for development.
ES3/Jscript Compatible: If we write any code that decides if we should load our JavaScript and CSS, that code will have to run successfully in any browser. Not just any modern browser; any browser. This means limiting ourselves to EcmaScript version 3, and also to the limitations of the not-quite compatible JScript. This code will be written like it’s 1999.
Let’s write some code
The simplest answer is to load our full experience with a module tag, and let it load everything.
<script type="module" src="./main.js"></script>
And maybe you can get away with that. But this is likely to leave you with a Flash of Unstyled Content, as your CSS won’t be loaded until your JavaScript runs.
We really want something like this:
if (browserIsGood) {
loadJavascriptAndCSS();
}
Okay, that’s a simple structure, but there’s a lot of handwaving happening there. Let’s dig in.
Loading JavaScript
Can we just use dynamic import?
if (browserIsGood) {
import("./main.js");
}
No, we can not! import is a reserved keyword in JavaScript. This will crash if you try to run it in a browser that doesn’t support dynamic import. We’ll have to load it the old fashioned way.
Everyone knows how to do this by now, right?
var target = document.getElementsByTagName("script")[0];
var head = target.parentNode;
function insertScript(src) {
var s = document.createElement("script");
s.setAttribute("src", src);
s.setAttribute("type", "module");
head.insertBefore(s, target);
}
if (browserIsGood) {
insertScript("./main.js");
}
Here we use the first script tag as the target for inserting, because the page might not have a head tag, and we create a script tag dynamically. Our JavaScript will be loaded asynchronously, so we’ll need to wait for DOMContentLoaded before we do anything. But these are table stakes for a modern web site these days. No big deal.
Loading CSS
So how can we load CSS dynamically without causing a FOUC?
var target = document.getElementsByTagName("script")[0];
var head = target.parentNode;
function insertStyle(src) {
document.write('');
}
if (browserIsGood) {
insertStyle("./main.css");
}
Wait, really? document.write? Yes. I’m sorry, but if you want to prevent a FOUC, you need there to be a link tag in the head before the parser sees the body. Browsers will block rendering in that case, and will not block rendering, leading to a FOUC in every other case.
Unless you want to get a little crazy and do it all yourself.
var target = document.getElementsByTagName("script")[0];
var head = target.parentNode;
var hide = document.createElement("style");
function hideDocument() {
hide.innerText = "body {display:none;}";
head.insertBefore(hide, target);
}
function handler() {
head.removeChild(hide);
}
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);
}
if (browserIsGood) {
hideDocument();
insertStyle("./main.css");
}
Here we add a style tag that hides the body tag, and then we remove it once the stylesheet loads (or fails to load). Note that we only call hideDocument once we know we’re inside a good browser.