Cover
#Though-Process#WebDevelopment

The Phantom Diagrams: How I Ditched Server-Side Headaches for Client-Side Magic

Abdul RafayAbdul RafayJun 26, 2025
3 min read read
📝

AI Article Summary

You know that moment. You’ve spent weeks building a blog or a portfolio you’re genuinely proud of. You test it locally, and everything looks perfect — animations, diagrams, responsiveness, all silky smooth. You deploy, expecting applause. But instead, you get build logs filled with cryptic errors and dependency hell. That’s exactly what happened to me when I decided to add dynamic Mermaid.js diagrams to my Astro-powered portfolio and blog.

Locally, it was a dream. Beautiful flowcharts and sequence diagrams rendered straight from fenced code blocks like ```mermaid — all converted into SVGs during the build process using Playwright.

But the second I hit deploy on Vercel?

The dream turned into dependency purgatory.

The Original Idea: Server-Side Rendering with Playwright

The concept was straightforward (in theory): Use Playwright during the build step to convert Mermaid code blocks into static SVGs. This would mean:

  • No Mermaid.js bundle on the client
  • Diagrams pre-rendered into the final HTML
  • No runtime dependency for rendering

So I added Playwright as a dependency, included a postinstall script to install the necessary browsers, and wrote a script that would scan Markdown content during the Astro build and render diagrams.

"scripts": {
  "postinstall": "npx playwright install"
}

This works great on your machine where apt-get exists and dependencies like libnss3 and libx11 are just a sudo away. But then Vercel laughed.

The Error Logs That Broke My Soul

Here’s what greeted me on deploy:

[23:21:07.099] sh: line 1: apt-get: command not found
[23:21:07.106] Failed to install browser dependencies
[23:21:07.107] Error: Installation process exited with code: 127

No apt-get, no system libraries, and absolutely no way for Playwright to run in the sandboxed Vercel environment. I even tried moving things into vercel.json:

{
  "build": {
    "env": {
      "INSTALL_PLAYWRIGHT_DEPS": "1"
    },
    "installCommand": "npx playwright install chromium"
  }
}

Still, the build failed during the actual execution — Playwright couldn’t launch its headless browser.

Temporary Fix: The Feature Flag Nobody Talks About

I hated doing this — but I disabled the diagram rendering altogether behind a feature flag just to get the blog live. That was my lowest point.

I had the content. I had the code. I had the visuals. But I couldn’t get them past the build system.

So I stopped trying to force it.

The Turning Point: What If the Client Did the Work?

Here’s the shift in mindset:

Why am I bending over backwards trying to make the server render something the client is fully capable of doing?

That’s when I decided to flip the architecture entirely.

Instead of generating diagrams on the server, I’d render them dynamically in the browser using React. And honestly? It worked better than I expected.

Building the MermaidRenderer React Component

I rewrote the logic as a proper MermaidRenderer.tsx component. The component does the following:

  1. Watches for code blocks with language-mermaid class.
  2. Dynamically imports Mermaid.js to keep the bundle size lean.
  3. Renders diagrams using mermaid.render.
  4. Injects React buttons into each diagram (copy to clipboard, fullscreen view).
  5. Listens for DOM changes using MutationObserver to handle lazy-loaded content or content rendered later.

Here’s the rough structure:

useEffect(() => {
  if (!mermaidInitialized) return;

  const renderDiagrams = async () => {
    const mermaid = (await import("mermaid")).default;
    const blocks = document.querySelectorAll(
      "pre code.language-mermaid:not([data-mermaid-rendered])"
    );

    for (let i = 0; i < blocks.length; i++) {
      const code = blocks[i].textContent;
      const svg = await mermaid.render(`diagram-${i}`, code);

      // Replace <pre><code> with the new SVG and inject buttons
      // ...
    }
  };

  renderDiagrams();

  const observer = new MutationObserver(/* watch for new content */);
  observer.observe(document.body, { childList: true, subtree: true });

  return () => observer.disconnect();
}, [mermaidInitialized]);

I also used ReactDOM.createRoot to inject the diagram buttons into each SVG block — buttons for fullscreen mode and clipboard copy.

And yes, the Mermaid theme is styled with my Tokyo Night color palette. 😎


The UX Wins

Switching to client-side rendering gave me more than just a successful deployment:

  • No More Build Failures: Vercel now builds flawlessly. My npm run build is back to being boring, as it should be.
  • User Interactivity: I added features like full-screen viewing and instant SVG copy. This would’ve been painful with server-rendered diagrams.
  • Performance: Mermaid.js is lazy-loaded. The script doesn’t even load unless there’s a Mermaid diagram present.
  • Future-Proofing: With React 19 and its new compiler optimizations, performance is only getting better. This setup already benefits from hooks like useCallback and memo.

The Architecture in Practice

Now, whenever a blog post has Mermaid code blocks, the following happens:

  1. The MermaidRenderer component activates on the client.
  2. It lazy-loads Mermaid.js.
  3. It parses the DOM for unrendered Mermaid blocks.
  4. It renders them into SVGs.
  5. React buttons (copy, fullscreen) are injected into each SVG container.
  6. The entire process runs after the page has loaded, without blocking the initial render.

Final Thoughts: When to Stop Fighting the Build System

This whole experience taught me a valuable lesson:

If your server-side setup is fragile, and the browser can handle it — let the browser do it.

Instead of brute-forcing headless rendering with Playwright, I leaned into the strengths of client-side React. Not only did it solve the problem, it made the solution better, faster, and more interactive.

So next time you’re debugging a server-side issue and nothing seems to work, ask yourself:

“Is this really the server’s job?”

You might find your answer — and your sanity — on the client side.

📢

Sponsored Content

💬 Join the Discussion

Share your thoughts and engage with the community

Loading comments...