Back to Blog
PerformanceJune 29, 20264 min read

Web Performance and Design Systems: Taming Bloat and Hydration

Web performance often suffers as design systems grow. Learn how to architect your component library to minimize CSS bloat and reduce costly hydration overhead.

Web PerformanceCSSReactHydrationDesign SystemsFrontend ArchitecturePerformanceWeb VitalsFrontend

We’ve all been there: you import a "simple" button component from your internal library, and suddenly your bundle size jumps by 40KB. Last year, while auditing a dashboard project, I found that our design system's global CSS file was bloated with unused utility classes, while our React components were forcing a full-page hydrate that took nearly 500ms on a mid-range mobile device. We were paying for features we weren't even using.

If you’re building a design system, your goal isn't just consistency—it's maintaining that consistency without destroying your site's metrics.

Why Design Systems Breed Web Performance Issues

The problem usually starts with "God components." Developers want a single <Button /> that handles every possible variant: primary, secondary, ghost, loading, icon-left, icon-right, and so on. When you bundle all that logic and styling, you end up shipping the full component payload to the browser, regardless of what the user actually sees.

In our case, we were shipping the entire library’s CSS in a single global stylesheet. Even if a page only used a header, it was still downloading the styles for complex data tables and modals. This is a classic case of CSS Architecture failing to scale.

We first tried to fix this by splitting the CSS into smaller files, but that led to cascading specificity issues and developers overriding styles with !important just to make things work. It was a mess.

Controlling CSS Architecture Bloat

To fix the CSS bloat, we moved to a "CSS-in-JS-lite" approach combined with atomic utility classes. Instead of monolithic stylesheets, we scoped styles to the component level using CSS Modules.

StrategyProsCons
Global CSSEasy to cacheMassive unused bloat
CSS ModulesScoped, tree-shakeableCan lead to class sprawl
Atomic CSSConsistent, smallHard to read/maintain
Zero-runtime CSSNo JS overheadLimited dynamic styling

By adopting zero-runtime libraries like Vanilla Extract, we moved the style generation to build time. The CSS is extracted into static files, meaning the browser only downloads the styles for the components actually rendered on the page.

The Hydration Trap

Even with perfect CSS, Hydration remains the silent killer of user experience. When you hydrate a complex tree of components, the browser must parse and execute all the JavaScript required to make the page interactive. This blocks the main thread, leading to poor Interaction to Next Paint: Architecting Deferred Hydration.

If you are using a framework like React, you should look into Selective Hydration and Islands Architecture for Better TBT. Instead of hydrating the entire document, you define "islands" of interactivity.

Flow diagram: Server Rendered HTML → Hydration Strategy; Hydration Strategy → Full Hydration; Hydration Strategy → Selective Hydration; Selective Hydration → Static Content; Selective Hydration → Interactive Island; Interactive Island → Lazy JS Bundle

When we implemented this, we stopped hydrating the static footer and hero sections. The TBT (Total Blocking Time) dropped by about 300ms, which was a massive win for our users on slower connections.

Balancing Flexibility and Performance

We also had to rethink our props. When a component accepts too many optional configurations, it becomes harder to memoize effectively. I’ve written previously about Strategic use of React.memo: Advanced Component Optimization, and the same principles apply here: keep components small and prop-focused.

If a component needs to be dynamic, ask yourself: does it need to be dynamic on the client, or can this be handled during Static Site Generation (SSG) Patterns: Architecting for Performance? Moving logic to the build step is almost always faster than doing it in the browser.

Key Takeaways

  1. Extract CSS at build time: Avoid runtime style injection if your design system allows it.
  2. Component Granularity: If a component is too heavy, break it into smaller sub-components that don't share a massive dependency tree.
  3. Defer Hydration: Don't ship JS for things that don't need it. Use React.lazy or equivalent patterns to split your bundles.
  4. Measure Early: Use Lighthouse and the Chrome DevTools Coverage tab to see exactly how much CSS and JS is unused on your landing pages.

I’m still experimenting with how much we can push to the edge, especially with Server Components. There’s a fine line between a modular system and a fragmented one that’s impossible to maintain. Start small, measure the impact of every new variant, and don't be afraid to delete unused code. Performance is a feature, not an afterthought.

Similar Posts