There are times when you get a request that seems easy enough. Say, designer wants user's name to display on header when user is logged in, and have login text instead when they are not. So you do that and all is well.
However in a typical React world there is a price to pay even in this little feature: you now have a layout shift, but only when user has logged in. This is most easily noticeable when you do a page refresh, forcing the client-side JavaScript to hydrate again:
With this particular name the shift isn't that great, but you can see language toggles on the left side shift a bit.
The white flash seen in the GIF helps to make it easier to see when the app gets hydrated. (And yes, it is also a bug. It has been exterminated.)
Why do we get that flash?
This is an issue we get from monolithic, single large client-side JavaScript app that is the current default from solutions like NextJS and Gatsby, which render the entire page upon hydrate. This will always be on the slow side no matter what you do: as long as you need to touch every part of the page and make sure it matches with what is generated with JavaScript, it will be slow, and the page will shift if there are conditionals depending on time or user.
However after the initial hydrate phase since most of the remaining page loads are done without HTML so the only way to see the problem is through refresh or by coming from another site. So as long as the problem is just this single case it isn't really worth it to fix it.
Time of Day
Late last year we got a new feature request: to display a different title on the front page depending on time of the day. We had a total of four different titles. But in addition to that there had to be a variant for logged in user! So in total that makes eight (8) different cases.
Of course the initial solution to this problem was the easiest and most straightforward one. Pick the correct variant and render that!
function TimeOfDayHeader({ titlesLoggedIn, titlesLoggedOut }) {
const userFirstName = useUserFirstName();
const hour = new Date().getHours();
const timeOfDay = (
(hour >= 4 && hour < 10 && TimeOfDay.Morning ||
(hour >= 10 && hour < 16 && TimeOfDay.Noon ||
(hour >= 16 && hour < 22 && TimeOfDay.Evening ||
TimeOfDay.Night
);
const titles = userFirstName
? titlesLoggedIn(userFirstName)
: titlesLoggedOut;
return <h1>{titles[timeOfDay]}</h1>;
}
It works fine: you get the title you want to see and all is well!
Until you notice the flaws.
- It is a
h1
level header that causes layout shifting, especially for users who stay logged in. The shift is worse on mobile. - We are dealing with time. So we do render one variant, but it doesn't get automatically updated as time passes. It isn't totally unlikely somebody opens a page at morning, and returns to it at evening.
- On SSG server renders only one variant when HTML pages are generated upon release. Thus, with four variants, the title in HTML is incorrect 75% of the day.
To fix the last issue you could make the site build automatically four times a day, and be like "problem solved" since that fixes the layout shifting from the viewpoint of automated tools.
But I don't like giving worse experience to users who are the paying customers.
Front-end tech to the rescue
What we would like to do is to immediately upon HTML load to:
- Choose the correct time of day elements to display.
- Given that we know user's name, apply user's name to all the correct places.
The problem is a bit hairy one to solve in universal app context, because in case of the likes such as Gatsby we have React that wants to control the entire app. And the main app bundle will always be a bit on the heavy side.
This leaves us with only one solution: we must go outside of the framework bundles.
Header's HTML
The first SSG requirement for time of day is to have all the eight variants rendered. Doing that is easy enough!
function TimeOfDayHeader({ titlesLoggedIn, titlesLoggedOut }) {
const userFirstName = useUserFirstName();
const userTitles = titlesLoggedIn(userFirstName);
return (
<h1>
<span data-time-of-day={TimeOfDay.Morning}>
<span>{userTitles[TimeOfDay.Morning]}</span>
<span>{titlesLoggedOut[TimeOfDay.Morning]}</span>
</span>
<span data-time-of-day={TimeOfDay.Noon}>
<span>{userTitles[TimeOfDay.Noon]}</span>
<span>{titlesLoggedOut[TimeOfDay.Noon]}</span>
</span>
<span data-time-of-day={TimeOfDay.Evening}>
<span>{userTitles[TimeOfDay.Evening]}</span>
<span>{titlesLoggedOut[TimeOfDay.Evening]}</span>
</span>
<span data-time-of-day={TimeOfDay.Night}>
<span>{userTitles[TimeOfDay.Night]}</span>
<span>{titlesLoggedOut[TimeOfDay.Night]}</span>
</span>
</h1>
);
}
Of course at this point we end up seeing all the eight different variants at once. This means we need something to hide the extra ones, and that is when CSS comes handy!
Controlling the Time of Day
What we need is a single place where we can tell the entire page the current time of the day. And we want to control it via CSS, because I think we can agree we already have plenty of JavaScript in the app. Or to think a bit differently: if the problem being solved is caused by having too much JS running on the client, does it make sense to solve such a problem by only writing more JS code?
I'm pointing this out only because that seems to be the norm these days!
This doesn't mean we have to avoid JS at all cost. We do need JS to know the time of the day. But since we're working on the web platform with web technologies and web standards, we should also make use of HTML and CSS.
The best place to touch is to set the time of day to <html />
element. With Gatsby we can do it in gatsby-ssr.js
like this:
function onRenderBody({ setHtmlAttributes }) {
setHtmlAttributes({ 'data-time-of-day': TimeOfDay.Noon });
}
But that only sets the initial attribute on SSG! But we can also add a related piece of JavaScript on the same file.
const timeOfDayAwareScript = `!function updateTimeOfDay(){
clearTimeout(updateTimeOfDay.timeout);
var hour = new Date().getHours();
var timeOfDay = (
(hour >= 4 && hour < 10 && '${TimeOfDay.Morning}') ||
(hour >= 10 && hour < 16 && '${TimeOfDay.Noon}') ||
(hour >= 16 && hour < 22 && '${TimeOfDay.Evening}') ||
'${TimeOfDay.Night}'
);
document.documentElement.setAttribute('data-time-of-day', timeOfDay);
updateTimeOfDay.timeout = setTimeout(updateTimeOfDay, (60 - new Date().getMinutes()) * 60000);
}()`;
function onRenderBody({ setHeadComponents, setHtmlAttributes }) {
setHtmlAttributes({ 'data-time-of-day': TimeOfDay.Noon });
setHeadComponents([
<script
key="time-of-day-script"
dangerouslySetInnerHTML={{ __html: timeOfDayAwareScript }}
/>
]);
}
What did we do here?
- We inject a script to
<head />
that is executed immediately upon HTML parsing. - The script code itself is IIFE, a function wrapper that executes itself.
- The code has "clever" re-use: it keeps calling itself once every hour.
-
clearTimeout
is a small safety feature to ensure there will never be more than one timeout.
The main thing however is that it sets data-time-of-day
attribute to the current time of the day. And it does it right at the beginning leaving no opportunity for layout shifting since we are guaranteed to have the right state even before <body />
element is parsed.
Styling the Time of Day
At this point we are still seeing all eight title variants. But we are now ready to add in some CSS!
const timeOfDayAwareCSS = `
html[data-time-of-day="${TimeOfDay.Morning}"] [data-time-of-day]:not([data-time-of-day="${TimeOfDay.Morning}"]),
html[data-time-of-day="${TimeOfDay.Noon}"] [data-time-of-day]:not([data-time-of-day="${TimeOfDay.Noon}"]),
html[data-time-of-day="${TimeOfDay.Evening}"] [data-time-of-day]:not([data-time-of-day="${TimeOfDay.Evening}"]),
html[data-time-of-day="${TimeOfDay.Night}"] [data-time-of-day]:not([data-time-of-day="${TimeOfDay.Night}"]) {
display: none;
}
`;
Tricky selectors? Well, a bit yes. What this selector does is to look at the root element's data-time-of-day
attribute, and then pick all the data-time-of-day
elements on the page that do not have the same value. And then hide them.
The good part about this selector is that we don't need to ever revert anything since it always only targets the elements we don't want to see.
The above CSS can be added to the HTML using setHeadComponents
similarly to the script. And after that we see titles only for the current time of the day!
Dealing with user's name
We are now down to seeing two titles at once: one for logged in user, and the other for logged out users. This is a point where we start hitting some further complexity, because server-side generated HTML should signal points where user's name is displayed.
To solve this we need to again make use of HTML attributes. But we also need to change the name. This means we need an additional element! So updating the header with data-first-name
:
function TimeOfDayHeader({ titlesLoggedIn, titlesLoggedOut }) {
// note: `userFirstName` is empty string when not known
const userFirstName = useUserFirstName();
const userTitles = titlesLoggedIn(userFirstName);
return (
<h1>
<span data-time-of-day={TimeOfDay.Morning}>
<span data-first-name={userFirstName}>{userTitles[TimeOfDay.Morning]}</span>
<span>{titlesLoggedOut[TimeOfDay.Morning]}</span>
</span>
<span data-time-of-day={TimeOfDay.Noon}>
<span data-first-name={userFirstName}>{userTitles[TimeOfDay.Noon]}</span>
<span>{titlesLoggedOut[TimeOfDay.Noon]}</span>
</span>
<span data-time-of-day={TimeOfDay.Evening}>
<span data-first-name={userFirstName}>{userTitles[TimeOfDay.Evening]}</span>
<span>{titlesLoggedOut[TimeOfDay.Evening]}</span>
</span>
<span data-time-of-day={TimeOfDay.Night}>
<span data-first-name={userFirstName}>{userTitles[TimeOfDay.Night]}</span>
<span>{titlesLoggedOut[TimeOfDay.Night]}</span>
</span>
</h1>
);
}
So far we haven't looked into what titlesLoggedIn(userFirstName)
looks like, but it is mostly irrelevant for us. But the result it generates should look like this:
return (
<>
Hello{' '}
<span
data-first-name={userFirstName}
data-to-content=""
>{userFirstName}</span>
!
<br />
Where would you like to travel?
</>
);
But now we have two attributes: data-first-name
and data-to-content
. Why is that?
Well, we will need to somehow indicate that we don't only want to update the attribute, but also the content of the element.
Updating user's name on page load
At this point we now need to update the user's name. This means another script. However this script has to be different to the previous one, because we need the DOM from the entire <body />
element to be parsed and ready to go.
There are two solutions: either inject the script to the end of the HTML document, or use type="module"
. Either works fine, but in this case I'll go ahead and prefer the type="module"
since it also allows us to avoid writing IIFE.
const firstNameScript = `
try {
const firstName = localStorage.firstName;
const els = Array.from(document.querySelectorAll('[data-first-name]'));
if (firstName && els.length) els.forEach((el) => {
el.setAttribute('data-first-name', firstName);
if (el.hasAttribute('data-to-content')) el.textContent = firstName;
});
} catch (error) {}
`;
localStorage
is not guaranteed to be available, and accessing it may throw. This is why we need the try...catch block.
Other than that the code is rather straightforward and minimal, which is good for code that is injected directly to every HTML page.
And once the script is injected to the page, in Gatsby's case again by using setHeadComponents
, we will now see no flicker as user's name is directly on the page!
Displaying only the correct title
We are now down to final bits of CSS. We need to pick which one to hide:
h1 > [data-time-of-day] > span[data-first-name=''] {
display: none;
}
h1 > [data-time-of-day] > span[data-first-name]:not([data-first-name='']) + span {
display: none;
}
Here we are again using the slightly tricky :not()
selector combo like before, this time targeting the element after to hide it when user's first name is known = user is logged in.
As this last piece of the puzzle hits in we only ever see one title, and have a layout shifting free experience!
The Final Words
This solution has a weakness: we now have code related to a single feature isn't neatly in one place, it is fragmented by nature and challenging to have clarity. Changing React code can break the layout shifting prevention. Or later on after team changes a person who doesn't know why a feature has been made may remove the layout shifting prevention JS and CSS as "ugly legacy code".
There is a way to work against these concerns: code organization, tests, and code comments describing what the purpose of the code is. I've used all three in hope that things will keep working in the future.
However I think most of the time we shouldn't have to resort into this kind of code trickery only to avoid layout shifting. You know, these days there are alternatives.
Instead of choosing NextJS or Gatsby for SSG you could also pick Astro with it's islands architecture, partial hydration, and support for many client-side tools. Why is it better? Well, despite not having used it yet, I think you wouldn't have the problems and challenges pointed out in this article!
You wouldn't have a single app wanting to hydrate a single point and take over everything. Instead you'd generate a static HTML on server-side, which would be taken over only when needed by much smaller apps, or widgets, inside the small islands all over the page. This means far less client-side JS executing upon initial page load. And less JS means faster execution, which means less opportunity for layout shifting to occur.
And all this while being able to use React, or Preact, or Svelte, or SolidJS.
Top comments (0)