Light/Dark mode and the bfcache
(Art by Kim Diaz Holm)
I had meant to write about the bfcache a while back, when I was first bitten by it, but I didn’t get around to it.
It came up in conversation with Zephyr last night, so figured I’d write about it now!
Light-Dark Mode
The way this website handles Light/Dark mode in the /settings page is a little complicated.
In my CSS, I use the light-dark() function to change the website’s colors based on the user’s operating system settings.
This bit is really simple:
:root {
color-scheme: light dark;
}
body {
background-color: light-dark(#fbecb2, #1f1f1f);
}Then, when the user changes the dark/light mode on their device (or when the device automatically changes it for them at sunset), the background color automatically changes.
You can give it a try right now, change your device’s color mode, and watch this website change colors!
For more details on how light-dark() works, check out
this fantastic article
by Sara Joy.
This all works pretty well (though I did have issues with older versions of Safari), and is probably what I’d recommend for most people. I decided to be extra though, and allow people the ability to override their OS settings.
If you go to my /settings page, you’ll see that you can choose between “Auto”, “Light”, and “Dark”. “Auto”, the default, respects the OS settings, but if you select “Light” or “Dark” you’ll force the website into that color mode.
This means that, for example, even if your computer is in light mode, you can choose that my website should be shown in dark mode.
To support this, and to persist the settings across viewings, I had to use some Javascript.
Persisting Settings
I use local storage to persist the selected color mode setting.
When you click one of the radio buttons, an event listener writes the setting to local storage, sets the color-scheme on the documen appropriately, and sets a custom CSS variable with my favicon URL to allow that to change colors as well.
Here’s the JS and HTML that I’m using:
function setColorAuto() {
document.querySelector(':root').style.colorScheme = 'light dark';
document.querySelector(':root').style.removeProperty('--favicon-url');
localStorage.setItem("color", "auto");
}
function setColorLight() {
document.querySelector(':root').style.colorScheme = 'light';
document.querySelector(':root').style.setProperty('--favicon-url', "url('/images/favicon.svg')");
localStorage.setItem("color", "light");
}
function setColorDark() {
document.querySelector(':root').style.colorScheme = 'dark';
document.querySelector(':root').style.setProperty('--favicon-url', "url('/images/favicon_white.svg')");
localStorage.setItem("color", "dark");
}<fieldset>
<legend>Color mode:</legend>
<div>
<input onchange="setColorAuto()" type="radio" id="colorAuto" name="color" value="auto" />
<label for="colorAuto">Auto</label>
</div>
<div>
<input onchange="setColorLight()" type="radio" id="colorLight" name="color" value="light" />
<label for="colorLight">Light</label>
</div>
<div>
<input onchange="setColorDark()" type="radio" id="colorDark" name="color" value="dark" />
<label for="colorDark">Dark</label>
</div>
</fieldset>Then I have a final piece of Javascript that runs on every page on
this website, which reads the local storage, and calls one of the above
setColor... functions as appropriate:
function loadFromLocalStorage() {
const color = localStorage.getItem("color");
switch (color) {
case 'light':
setColorLight();
break;
case 'dark':
setColorDark();
break;
default:
setColorAuto();
}
// more code for the contrast setting here...
}(All code heavily inspired by Sara Joy’s article)
And bam! Now you can override the default system color theme, and the website will remember your choice when you next visit.
But there was one problem: if you were on my a page on my website (in Chrome or Firefox), and then went to /settings, changed your color mode, and then hit the back button in your browser – it wouldn’t work! The page you went back to would still be on the old theme.
Fortunately there was an easy fix, but I had to learn about a new browser feature to figure it out.
The bfcache
The Back/Forward cache (bfcache) is a new feature of modern web browsers that aims to enable instant back and forward navigation.
Before bfcache, when you hit back or forward, the browser would tear down the page you were currently on, deleting any Javascript state, saved images, etc, and load the new page you’re going to.
Given you’re going backwards or forwards to a page you’ve already been on, the resulting load would hopefully be quick, with resources (e.g. images) stored in the browser’s cache, meaning there shouldn’t need to be another round trip across the network.
Unfortunately, a whole lot of the web is built using bloated Javascript, which can take a long time to re-run, even if there’s no network requests. Additionally, increasing amounts of state are being stored client-side, which can be erased on page reload.
So the browser people made the bfcache, which stores the entire state of a page, Javascript heap and all, and uses it to recreate pages on back/forward navigation.
This web.dev article goes into depth about how it works.
Loading a page from the bfcache is pretty much instant, which is nice, but, by design, it means Javascript that was run on initial page load won’t be run again.
This breaks my color mode picker, because the
loadFromLocalStorage function above doesn’t get re-run when
navigating back from the settings page.
Fortunately, there’s a real easy fix!
I just needed to add an event listener for the window’s
pageshow event, which gets fired when a page is restored
for the bfcache, and call my function there:
window.addEventListener('pageshow', () => {
loadFromLocalStorage();
});There’s the possibility for a slight flicker as the page is restored from the bfcache in one color mode, and then quickly changed to the next, but I haven’t noticed it on any of my devices (if you notice it, please let me know!)
In Conclusion
This was a tricky one! The browser ecosystem is constantly changing, and you often don’t find out about new features like bfcache until you run into them in the wild.
I’m glad there was an easy fix for my situation – I’ve run into this bug on other people’s websites that use similar color mode changing, so hopefully it’ll work for them too.
I have mixed feelings about bfcache. Part of me is just yelling: “if we made websites better, we wouldn’t need this!”.
But sadly, we don’t make websites well. In our AI / JS slop world, people increasingly have to deal with slow-ass buggy websites that take up way too much time and bandwidth.
Given the state of things, bfcache is probably a big help.
But man, I wish the state of things were better!