How to stop Theme flickering in Fresh
5/24/2023, 6:04:26 PM

In the previous post, we built a simple Theme Switcher using Preact and Fresh on top of Deno. Two things were missing in that implementation we are going to fix both of them now:
To understand how to fix both of those problems, we first need to understand why they happened in the first place.
The Island Architecture
The Islands Architecture is a very interesting design pattern. You serve static HTML content (the "ocean") to the client's browser and only hydrate a little of Javascript (the "island") in specific portions of the User Interface, upon user request. By default, this approach will never ship any Javascript to the client, essentially creating zero overhead on served pages, which leads to more performant pages with higher Lighthouse scores.

Fresh wasn't the first to come up with this idea, mind you. Remix (from the same creators of React Router (!))had already started working on this in early 2022, they even tried to sell a license to use the framework, which didn't work well for them.
So by now, you might have realized that not shipping Javascript by default has its drawbacks. If the page first needs to load before it can download the Javascript to change the theme colors, then we can't have our theme applied from the get-go, with no choice but to have our clients have to put up with the flickering, right? Well, not exactly...
Opting out of the Islands Architecture
It's possible that we can ship the required Javascript on every page, but in doing so, you need to understand the tradeoffs:
- You introduce consistent overhead to every page load, which will progressively worsen your Lighthouse page performance score, the more you do it.
- You are deviating from the main design choice for the framework, which means that you will not find a lot of resources to do things this way from this point onwards. If you have questions, you will have to mostly figure something out by yourself.
Adding a script file to every response
Be very careful about the script
tags that you import on your project. Not knowing what you are doing can leave you (and your users) vulnerable to Cross Site Scripting attacks (XSS, for short). Make sure that you properly review any code that suggests using these and, if in doubt, don't use them in your project!
In our case, our implementation is incredibly simple. We just add a small script to the response's <head>
that checks if the user has a theme saved in localStorage
and if they don't, we try to apply their OS-preferred color scheme. Let's have a look:
And inside the script file:
In order:
Check if there is a theme already saved on
localStorage
. If there isn't one, check what's the user preferred color scheme, save it, and setwindow.showDarkMode
. If there is, you just setwindow.showDarkMode
on/off based on the saved theme.Check
window.showDarkMode
and apply the colors to theroot
element for either mode based on that beingtrue
orfalse
.
Now all we gotta do is update our component and we are done!
Because window.showDarkMode
is set within the response's <head>
, Typescript doesn't know that it exists and will give you a warning, hence the suppression above. Since your useEffect()
no longer needs to set the theme based on what is saved on localStorage
, we can remove that bit too, leaving the initial check to only validate if it's the first run and skip when it is.
So there you have it, a Theme Switcher that sets the correct theme, acknowledges the user's preferences, and doesn't flicker on the initial load.
Alternatives to using script tags
What other ways could you possibly implement a Theme Switcher without needing to script files on every request?
One of the options would be to use route-based theming. You could create your entire website nested in either a /light
or /dark
route (or better yet, a single /[theme]
route!), and have your index redirect to either of those based on their OS color-scheme preference. The drawback to this approach is that you can't give the users a pretty theme transition when they switch themes, since that will force an entire page reload when they get redirected, but regardless, it's still a possibility that you could implement.
Another option would be to not save the theme to localStorage
and also never redirect, essentially turning your application into a SPA, making your users navigate through pages using React Router instead. The drawback to this approach would be to have the theme reset on every visit, which would be annoying if people had to come back to your website regularly. If you are just creating a portfolio website that is meant for recruiters to check once and dip, that is not a big deal, but for a blogging website (like this one!), it's pretty much unacceptable. This option also isn't really what Fresh is going for either, so at this point you might as well just use another framework entirely instead, maybe even going full NextJS + React instead.