DEV Community

Sulagna Ghosh
Sulagna Ghosh

Posted on

How a Broken Logo Animation Taught Me How Next.js Really Boots

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)
Enter fullscreen mode Exit fullscreen mode

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... */
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

"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';
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 ✅
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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; }
Enter fullscreen mode Exit fullscreen mode

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="" />
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
      }
    })();
  `}}
/>
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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);
}, []);
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

DCO is a requirement in CNCF projects. Every commit must include:

Signed-off-by: Your Name <your@email.com>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

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)