DEV Community

Cover image for My Recent Debugging Adventure: The Case of the Disappearing Stylesheets
Sibasish Mohanty
Sibasish Mohanty

Posted on

My Recent Debugging Adventure: The Case of the Disappearing Stylesheets

You know those days where a tiny bug eats half your brainpower, and then when you finally solve it, you feel like both an idiot and a wizard? Yeah. This is one of those stories.

The Setup: Performance Optimization Gone Wrong

Like any good frontend engineer, I wanted to optimize stylesheet loading. So instead of letting stylesheets block render, I tagged them like this:

<link rel="stylesheet" media="print" href="/assets/css/base.css" />
Enter fullscreen mode Exit fullscreen mode

The trick here: browsers skip applying media="print" styles until you explicitly flip it to all. That way, the stylesheet downloads in the background, but doesn’t block rendering.

And then I had a little script to flip the switch:

document
  .querySelectorAll('link[rel="stylesheet"][media="print"]')
  .forEach(link => {
    const enableMedia = () => {
      link.media = 'all'
      link.removeEventListener('load', enableMedia)
    }
    if (link.sheet) {
      enableMedia()
    } else {
      link.addEventListener('load', enableMedia)
    }
  })
Enter fullscreen mode Exit fullscreen mode

Beautiful, right? Except … nothing happened. Stylesheets never applied. My page looked naked. Not in a good way 😜.

The Bug: CSP Meets Timing

First instinct: CSP. I had a pretty strict Content Security Policy with script-src 'self' 'strict-dynamic'. Inline hacks were out, so the loader script was external.

But then I noticed something curious: my script was being deferred.

<script defer src="/static/js/stylesheetloader.js"></script>
Enter fullscreen mode Exit fullscreen mode

defer means “wait until HTML parsing is done before running this script”. By then, the browser had already seen my <link media="print"> tags, queued them, and … didn’t trigger load the way I expected. My script showed up too late to catch the party.

The Fix: Async to the Rescue

The fix was almost laughably simple:

<script async src="/static/js/stylesheetloader.js"></script>
Enter fullscreen mode Exit fullscreen mode

Switching defer to async made the script run as soon as it was available, before the render pipeline locked things down. Boom 💥. Stylesheets applied perfectly.

Sometimes all it takes is one attribute flip to go from “WTF why is CSS broken” to “I am a genius.”

Why This Works

Let’s zoom out:

  • defer scripts wait until after HTML parsing is complete, keeping order intact.
  • async scripts run as soon as they’re downloaded, ignoring order.

In my case, I didn’t care about execution order. I just needed the loader to run ASAP. Async was the perfect fit.

Lessons Learned

  1. Performance hacks are sharp knives. Using media="print" for async stylesheets works, but you need to manage timing carefully.
  2. CSP adds invisible complexity. With inline scripts off the table, your timing depends entirely on how you load external JS.
  3. defer vs async actually matters. Don’t just cargo-cult defer everywhere. Think about when you need the script to run.

Generalizing the Problem

This isn’t just about my stylesheet loader. It’s a broader lesson about script execution timing under CSP:

  • If you have critical scripts that need to act before rendering decisions (like stylesheet toggles, dynamic meta updates, etc.), use async or preload strategies.
  • If execution order matters (like multiple polyfills), then defer is safer.
  • If you’re battling CSP, externalize your hacks, don’t rely on inline event handlers (onload="...").

Alternatives Worth Considering

  • <link rel="preload">: preload stylesheets and apply later.
  • Modern bundlers (Rsbuild, Vite): can inline critical CSS and lazy-load the rest.
  • JS-based style injection: less common, but some teams prefer runtime style injection for full control.

Wrap-up

Sometimes, a bug isn’t about broken code. It’s about tiny mismatches in timing and browser behavior. This one was a perfect storm of CSP, defer, and stylesheet loading tricks.

I wasted hours chasing ghosts, but in the end, flipping one attribute fixed everything. The kind of debugging adventure that keeps you humble … and slightly amused.

So next time your CSS looks naked, check your <script> attributes. 😉

Top comments (0)