A deep dive into pre-hydration rendering, critical CSS, vanilla JS, and what it actually means to contribute to open source
By Sulagna Ghosh | Open Source Contributor, Meshery
I came to this bug expecting to spend 20 minutes adding a few lines of CSS.
Several days later, I had learned more about how Next.js actually works than I had in months of using it. I had navigated a Turbopack internal error, discovered why CSS imports can arrive too late, traced a root cause through 2022 commit history, understood the difference between two completely separate loading screens in the same codebase, tried a JS solution that was almost right, implemented an inline CSS fix, defended it through code review, and then collaborated with a maintainer to produce a solution that combined both approaches elegantly.
This is the full story — including the wrong turns.
The Issue
GitHub Issue #18210 was straightforward on the surface:
"We previously had an animation of the Meshery logo on the initial page load, which is currently not working."
When you navigated to playground.meshery.io, you saw the Meshery logo — a circle made of 25 teal triangles — sitting perfectly still in the center of a dark screen. The logo was there. The text was there. But nothing moved.
The expected behavior: the 25 triangles should fill in one by one sequentially — a staggered reveal animation where each triangle transitions from transparent to teal, filling in over about 2.4 seconds, then fading out and repeating. A beautiful ripple effect across the circular mesh logo, cycling continuously while the app loaded in the background.
It looked complete. It was completely broken.
First Contact — Finding the Right File
The first task in any unfamiliar codebase is navigation. Following the import chain from the entry point:
_document.tsx
└── PureHtmlLoadingScreen (from LoadingComponentServer.tsx)
└── AnimatedLogoDark (from ui.config.js)
└── AnimatedMesheryDark (from AnimatedMesheryCSS.tsx)
I found AnimatedMesheryCSS.tsx. It had the full SVG — all 25 triangle polygons, each with a className like svg-meshery-1, svg-meshery-2, and so on. The structure was right. The logo would render correctly.
But there were no styles. No @keyframes. No transition. No fill. Nothing that would make those triangles animate. The classes existed. The elements existed. But the stylesheet that should have brought them to life was simply absent.
"Easy," I thought. "Just add the CSS."
The CSS File That Already Existed
Before writing anything new, I explored the codebase further. A good rule in open source: never write code that already exists.
I found it: ui/pages/styles/AnimatedMeshery.css
svg .svg-meshery-1 {
fill: transparent;
-webkit-transition: fill 0.7s cubic-bezier(0.47, 0, 0.745, 0.715) 0.8s;
transition: fill 0.7s cubic-bezier(0.47, 0, 0.745, 0.715) 0.8s;
}
svg .active.svg-meshery-1 {
fill: rgb(0, 211, 169);
}
svg .svg-meshery-2 {
fill: transparent;
transition: fill 0.7s cubic-bezier(0.47, 0, 0.745, 0.715) 0.9s;
}
svg .active.svg-meshery-2 {
fill: rgb(0, 211, 169);
}
/* ...continues for all 25 triangles up to 3.2s delay... */
The file header said it was generated by SVG Artista on September 14, 2022. The animation mechanism was elegant — each triangle starts as fill: transparent, and when the active class is added, a CSS transition fills it with teal. Each triangle had a slightly longer transition-delay — starting at 0.8 seconds and incrementing by 0.1 seconds all the way to 3.2 seconds for the last triangle. Two shades of teal alternated: rgb(0, 211, 169) — a brighter mint — and rgb(0, 179, 159) — a deeper teal.
This CSS was already imported in _app.tsx:
import './styles/AnimatedMeshery.css';
"Perfect. The CSS exists. It's already imported. I just need to use it."
I was about to learn why that would not work.
My First Attempt — Vanilla JS
Before trying the CSS import, I actually tried fixing it with JavaScript first. My instinct was: loadingMessages.js already loads synchronously before React — maybe I can manipulate the DOM from there.
I tried some clearTimeout based approaches in loadingMessages.js to control the animation state. It didn't work. Things were unclear at that stage and I couldn't see exactly why, so I moved on and tried a different approach.
That instinct — as it turned out — was completely correct. I just didn't push it far enough. More on that later.
The Import That Failed
My next attempt was the most obvious one:
import '../../../pages/styles/AnimatedMeshery.css';
The error was not a missing file or path error. It was this:
Error [TurbopackInternalError]: internal error: entered unreachable code:
there must be a path to a root
Debug info:
- Execution of get_written_endpoint_with_issues_operation failed
- internal error: entered unreachable code: there must be a path to a root
at turbopack/crates/turbopack-core/src/module_graph/mod.rs:662:25
A Turbopack internal error. The bundler itself was crashing.
But even if it hadn't — would the import have actually worked?
Understanding What Was Actually Happening
To answer that, I needed to understand something fundamental about how Next.js applications actually start.
Most developers think of a Next.js app as one thing that loads. It is actually two completely separate phases with different rules.
Phase 1 — The HTML Shell (_document.tsx)
_document.tsx runs on the server and generates the raw HTML the browser receives. This HTML is sent before any JavaScript has loaded, before any CSS bundle has been processed, before React has done anything at all.
When the browser receives this HTML and starts painting pixels — that is the pre-hydration phase. React is not running yet. No JavaScript is executing. No CSS bundle has loaded. PureHtmlLoadingScreen is rendered inside _document.tsx, which means it appears during this phase as pure static HTML.
Phase 2 — The React App (_app.tsx)
_app.tsx loads after the browser downloads the JavaScript bundle, parses it, and executes it. React hydrates the page, hooks run, state initializes, and CSS imports become active. Everything developers normally think about when they think about React lives here.
The Critical Gap
0ms Browser receives HTML from server
→ _document.tsx output paints immediately
→ PureHtmlLoadingScreen is VISIBLE
→ AnimatedMesheryCSS.tsx SVG is in the DOM
→ AnimatedMeshery.css is NOT loaded yet
200ms- JavaScript bundle downloads
2000ms → _app.tsx executes
→ import './styles/AnimatedMeshery.css' runs
→ CSS bundle loads
→ But PureHtmlLoadingScreen is already showing
without animation this entire time
The loading screen shows during the gap between 0ms and the moment the JS bundle loads. During that entire window, AnimatedMeshery.css does not exist from the browser's perspective. The triangles sit as transparent shapes, waiting for styles that will never arrive in time.
This is why the import in _app.tsx doesn't help AnimatedMesheryCSS.tsx. They live in different worlds. And a <link> tag wouldn't help either — it's an asynchronous network request that arrives 50-200ms late at minimum.
The only way to guarantee styles are present at 0ms is to embed them directly in the HTML itself.
Two Loading Screens, Not One
This investigation revealed something else I hadn't noticed initially: there are not one but two completely separate loading screens in the Meshery codebase.
Loading Screen 1 — Pre-Hydration (the broken one)
_document.tsx
→ PureHtmlLoadingScreen
→ AnimatedMesheryCSS.tsx
Shows before React loads. Must be 100% self-contained. No hooks, no CSS imports, no Emotion.
Loading Screen 2 — Post-Hydration (the working one)
_app.tsx (imports AnimatedMeshery.css)
→ DynamicFullScreenLoader
→ LoadingScreen.tsx
→ AnimatedMeshery.tsx (uses useState/useEffect to toggle 'active' class)
Shows while the app is fetching data after React has already loaded. Works correctly because React is running and the CSS bundle is available.
This distinction matters. Meshery is not a simple website — it is a cloud native management platform for Kubernetes. On boot it initializes Kubernetes contexts, capabilities registry, GraphQL subscriptions, permissions, Prometheus/Grafana connections. This can take 3-8 seconds. A blank screen for that long looks broken. Most Next.js apps never need a pre-hydration loader because they boot fast enough that the gap is invisible. Meshery does need one.
The Root Cause — Traced to a 2022 Commit
While investigating, I checked the commit history for AnimatedMeshery.css:
https://github.com/meshery/meshery/commits/master/ui/pages/styles/AnimatedMeshery.css
I found commit b09b386 from December 13, 2022.
Before that commit, each animation component had its CSS co-located:
// AnimatedMeshery.js — before b09b386
import "./animatedMeshery.module.css"; // lived next to the component ✅
That commit moved all CSS files to pages/styles/ and imported them globally in _app.js, removing the local imports from each component. At the time, this was fine — PureHtmlLoadingScreen didn't exist yet.
Later, PureHtmlLoadingScreen was introduced in _document.tsx using AnimatedMesheryCSS.tsx. Nobody connected that this new pre-hydration component depended on CSS that had been moved to _app.js — where it would load too late.
The Next.js and MUI v7 upgrades didn't cause the bug. They made an existing architectural mismatch visible. The mismatch had been there since late 2022, silently waiting.
My Fix — Inline CSS
With the diagnosis clear, my solution was to inline the original CSS directly inside AnimatedMesheryCSS.tsx using a <style> tag:
const ANIMATION_STYLES = `
[class^='svg-meshery-'] {
fill: transparent;
transition: fill 0.7s cubic-bezier(0.47, 0, 0.745, 0.715);
}
.svg-meshery-1 { transition-delay: 0.8s; }
.svg-meshery-2 { transition-delay: 0.9s; }
/* ...all 25 triangles... */
svg .active.svg-meshery-1 { fill: rgb(0, 211, 169); }
svg .active.svg-meshery-2 { fill: rgb(0, 211, 169); }
/* ...active fills for all 25... */
`;
const AnimatedMeshery = (props) => {
return (
<div>
<style dangerouslySetInnerHTML={{ __html: ANIMATION_STYLES }} />
<svg ...>
<polygon className="svg-meshery-1 active" ... />
{/* all 25 triangles with active class hardcoded */}
</svg>
</div>
);
};
A <style> tag with inline content is part of the HTML document itself. The browser parses and applies it in the same pass as the HTML — available at 0ms, before any network request or script executes.
This is the same pattern already used in _document.tsx for scrollbar styles:
<style type="text/css">
{`
.hide-scrollbar::-webkit-scrollbar { width: 0 !important; }
.reduce-scrollbar-width::-webkit-scrollbar { width: 0.3em !important; }
`}
</style>
The animation fix followed the exact same pattern — just with more CSS.
I also noted that AnimatedMeshery.css should NOT be removed — it is still imported in _app.tsx and powers the post-hydration LoadingScreen component. Both files serve different moments in the application lifecycle.
The Code Review
After opening the PR, the automated Gemini Code Assist bot suggested a CSS refactor:
/* Before — properties repeated 25 times */
svg .svg-meshery-1 { fill: transparent; transition: fill 0.7s ...; }
/* After — shared properties extracted once */
[class^='svg-meshery-'] {
fill: transparent;
transition: fill 0.7s cubic-bezier(0.47, 0, 0.745, 0.715);
}
.svg-meshery-1 { transition-delay: 0.8s; }
The [class^='svg-meshery-'] selector matches any element whose class attribute starts with svg-meshery-. It works here because all animated elements follow that naming convention. One subtlety worth knowing: if any element had a prefix class like active svg-meshery-1 instead of svg-meshery-1 active, the ^= selector would silently miss it. Understanding why the selector works — not just that it works — matters for maintainability.
The maintainer also asked me to investigate the root cause more deeply before merging, which led to finding commit b09b386 and the full architectural explanation above.
The Maintainer's Refinement — The Complete Solution
After my inline CSS fix was reviewed, the maintainer pointed to something I had missed. Look at _document.tsx:
{/* Pre-React script - must be sync to run before React hydration */}
<script src="/loadingMessages.js"></script>
<PureHtmlLoadingScreen id={'PRE_REACT_LOADER'} message="" />
loadingMessages.js is a synchronous script that loads before PureHtmlLoadingScreen renders. It already controlled the loader's show(), hide(), and setMessage() functions. The maintainer's insight: this file was the natural home for the animation toggle logic too.
They provided a new initializePreReactLoader function to add to loadingMessages.js:
var _preReactAnimationInterval = null;
function stopPreReactAnimation() {
if (_preReactAnimationInterval) {
clearInterval(_preReactAnimationInterval);
_preReactAnimationInterval = null;
}
}
function initializePreReactLoader() {
// Set the loading message
try {
const loaderMessage = getLoaderMessageNode();
if (loaderMessage) {
loaderMessage.textContent = PersistedRandomLoadingMessage();
}
} catch (e) {
console.log('Failed to set loading message', e);
}
// Replicate the original useState/useEffect toggle — in vanilla JS
try {
const prerenderLoader = getLoader();
if (!prerenderLoader) return;
const parts = prerenderLoader.querySelectorAll(
'[class^="svg-meshery-"], [class*=" svg-meshery-"]'
);
if (!parts.length) return;
let isActive = true;
const setActive = function(nextActive) {
for (let i = 0; i < parts.length; i++) {
parts[i].classList.toggle('active', nextActive);
}
};
// Turn off after 100ms — same as original useEffect
setTimeout(function() {
isActive = false;
setActive(isActive);
}, 100);
// Toggle every 4 seconds — same as original useEffect
stopPreReactAnimation();
_preReactAnimationInterval = setInterval(function() {
if (!prerenderLoader.isConnected || prerenderLoader.style.display === 'none') {
stopPreReactAnimation();
return;
}
isActive = !isActive;
setActive(isActive);
}, 4000);
window.addEventListener('beforeunload', stopPreReactAnimation, { once: true });
} catch (e) {
console.log('Failed to initialize pre-react logo animation', e);
}
}
And in _document.tsx, the old bottom script is replaced:
<script
dangerouslySetInnerHTML={{ __html: `
(function () {
try {
if (window.Loader?.initializePreReactLoader) {
window.Loader.initializePreReactLoader();
}
} catch (e) {
console.log("Failed to run pre-react loader setup", e);
}
})();
`}}
/>
This is the complete solution. The inline CSS provides the transition definitions. The vanilla JS in loadingMessages.js toggles the active class — replicating exactly what useState and useEffect did in AnimatedMeshery.tsx, but without needing React at all.
The Moment I Realised I Had Been Right All Along
When I saw the maintainer's solution, I remembered something. My very first attempt at this bug had been a JS approach — trying to control the animation from loadingMessages.js using clearTimeout. It hadn't worked and I had moved on.
The reason it hadn't worked was not that the JS approach was wrong. It was that the CSS transitions weren't defined yet — so even if the JS had successfully toggled the active class, there were no styles saying what active should do. An invisible toggle.
What I actually needed — and what neither I nor the maintainer figured out alone — was both pieces together:
My contribution → Inline CSS in AnimatedMesheryCSS.tsx
(defines what 'active' visually means)
Maintainer's part → initializePreReactLoader in loadingMessages.js
(toggles 'active' class via vanilla JS)
Neither works without the other. My inline CSS fix was not wrong — it was half of the complete solution. The maintainer provided the other half. That is how collaborative open source development works.
The setInterval Bug — And Why the Bot Was Right
During the process the maintainer suggested converting setTimeout to setInterval in the React hook versions of the component (AnimatedMeshery.tsx and AnimatedLightMeshery.tsx). I made that change but the automated code review bot immediately flagged it as a logic error — correctly:
// WRONG — setInterval fires every 100ms forever
useEffect(() => {
const interval = setInterval(() => {
setIsActive(false); // overrides the 4s toggle every 100ms
}, 100);
return () => clearInterval(interval);
}, []);
The 100ms interval continuously sets isActive to false, which overrides the 4-second toggle entirely — the animation gets stuck in the inactive state forever.
// CORRECT — setTimeout fires once then stops
useEffect(() => {
// Effect designed to run only once on component mount
const timer = setTimeout(() => {
setIsActive(false);
}, 100);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
const timer = setTimeout(() => {
setIsActive((prev) => !prev);
}, 4000);
return () => clearTimeout(timer);
}, [isActive]);
setTimeout fires once and stops. Each time isActive changes, the second useEffect runs again, setting a new 4-second timer. This creates the continuous cycle correctly. The bot was right. I reverted.
The DCO Sign-off Problem
After pushing changes, the DCO (Developer Certificate of Origin) check failed:
There are 2 commits incorrectly signed off.
DCO is a requirement in CNCF projects. Every commit must include:
Signed-off-by: Your Name <your@email.com>
The fix:
git stash # save uncommitted changes
git rebase HEAD~2 --signoff # add signoff to last 2 commits
git push --force-with-lease origin fix-meshery-logo-animation
git stash pop # restore changes
For future contributions, always use git commit -s — the -s flag appends the Signed-off-by line automatically.
What This Bug Actually Touched
What appeared to be a simple CSS animation issue intersected with a surprisingly wide range of concepts:
| Concept | How it appeared |
|---|---|
Next.js _document.tsx vs _app.tsx
|
Two different render phases with different rules |
| Pre-hydration vs post-hydration rendering | When the browser paints vs when React runs |
| Critical CSS and FOUC prevention | Why inline styles are the only correct approach here |
Emotion SSR extractCriticalToChunks
|
Why it captures some styles but not others |
CSS transitions vs @keyframes
|
The original mechanism and how to replicate it |
CSS attribute selectors [class^=]
|
Selecting by class prefix, and when it's safe |
dangerouslySetInnerHTML |
The React pattern for inline <style> tags |
| Synchronous vs bundled scripts | What loads at 0ms vs what loads at runtime |
| Vanilla JS pre-hydration logic | Using loadingMessages.js as the designated pre-hydration JS |
setTimeout vs setInterval in React |
Why they behave very differently in useEffect
|
| Git DCO and commit signing | Open source contribution requirements |
git stash + git rebase --signoff
|
Rewriting commit history safely |
| Commit archaeology | Tracing a regression to its origin |
The Broader Lesson
The best bugs to fix in open source are not the ones with obvious solutions. They are the ones that force you to understand the system before you can fix it correctly.
My first solution — inline CSS — was architecturally correct and solved the problem. The maintainer's refinement — moving the toggle logic to loadingMessages.js — made it more elegant by using existing infrastructure and restoring the original cycling behavior exactly. My solution was not wrong. It was incomplete. That is a very different thing.
What actually makes a contribution valuable is not whether your first solution is perfect. It is whether you understand the problem deeply enough to explain it, defend it, refine it, and learn from the collaboration. That understanding shows in how you write your PR description. It shows in how you respond to review comments. It shows in your ability to trace a regression to a 2022 commit and explain the full chain of events that led to it.
In open source, that depth is visible. And it is what gets you noticed.
Getting Started With Meshery
If you want to contribute to Meshery:
- GitHub: github.com/meshery/meshery
- Contributing Guide: docs.meshery.io/project/contributing
- Community Slack: slack.meshery.io
- Good First Issues: Labelled on GitHub — great starting point
The community is genuinely welcoming. Drop into Slack. Ask questions. Share what you find. The learning happens in public — and that is exactly where it should happen.
If this post helped you understand something about Next.js, pre-hydration rendering, or open source contribution, consider starring the Meshery repository and picking up a good-first issue.
Tags: next.js open-source meshery cncf css react pre-hydration ssr contributing lfx-mentorship vanilla-js debugging
Top comments (0)