How to keep dark mode fast

Why performance matters for dark mode

Dark mode should feel instant and unobtrusive. If toggling theme adds measurable delay, causes layout shifts, or increases page load time, users perceive the site as sluggish and unreliable. For Mediumish — a content-first theme — readers expect fast rendering and smooth interactions. A slow dark mode can lead to higher bounce rates, poorer user engagement, and lower perceived quality.

Performance matters not only for UX but also for SEO: slower pages often score lower on Core Web Vitals and search rankings. The goal is to add dark mode while keeping initial paint, interaction readiness, and cumulative layout shift (CLS) under control.

Common performance pitfalls

Before implementing optimizations, know where slowdowns usually come from:

  • Large or blocking CSS: Loading big stylesheets that must be parsed before paint.
  • Late theme detection: Relying on full JS bundles to decide theme, causing flash of wrong theme or delayed switch.
  • Multiple large image assets: Serving extra files for dark mode without lazy loading or optimized formats.
  • Heavy JavaScript toggles: Running expensive DOM operations when toggling theme.
  • Unnecessary repaints or reflows: Triggering style changes in ways that force layout recalculation for many elements.

Critical CSS strategy

Keep the initial CSS minimal and critical for above-the-fold content. This reduces time to first meaningful paint and lets users see a usable page quickly.

Steps to apply:

  1. Extract a small critical CSS chunk that covers body background, typography, header, and the toggle control. Inline it in the document head to avoid a render-blocking request.
  2. Place the CSS variables and the minimal theme-related styles in that inline critical CSS so the correct colors can apply immediately.
  3. Load the full stylesheet asynchronously (for example, using rel="preload" as="style" onload="this.rel='stylesheet'"), ensuring the page becomes functional while full styles load in the background.
<style>
/* Critical styles only */
:root{ --bg:#fff; --text:#111; }
:root[data-theme="dark"]{ --bg:#0b0f12; --text:#e6eef8; }
html,body{ background:var(--bg); color:var(--text); }
</style>

<link rel="preload" href="/css/main.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/main.css"></noscript>

Fast theme detection and FOIT prevention

To avoid flash of wrong theme and keep paint fast, run a tiny synchronous script at the top of the head that reads the saved preference and sets a data-theme attribute before the browser paints.

Keep this script under ~1 KB. It should not import external code or rely on frameworks. The goal is to set the theme attribute so CSS variables apply immediately.

<script>(function(){
try{
  var t=localStorage.getItem('site-theme-preference');
  if(t){ document.documentElement.setAttribute('data-theme', t); }
  else if(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches){
    document.documentElement.setAttribute('data-theme','dark');
  }
}catch(e){};})();</script>

This tiny script prevents layout jank and removes the need for heavier JS to run before paint.

Minimizing assets and safe swapping

Avoid loading duplicate or heavy assets for dark mode unless necessary. When you must swap assets (icons, logos, illustrations), do it efficiently:

  • Prefer SVG icons with CSS-controlled fills so a single file adapts to both themes without swapping.
  • For raster images that need replacements, lazy-load the alternate asset only when theme changes and the image is visible in the viewport.
  • Use modern image formats (WebP, AVIF) and provide properly sized sources via srcset to reduce bytes.
<img src="/img/logo-light.svg" data-dark="/img/logo-dark.svg" class="theme-aware" alt="Site logo">

<script>
document.addEventListener('DOMContentLoaded',function(){
  var imgs=document.querySelectorAll('img.theme-aware');
  imgs.forEach(function(img){
    var dark=img.getAttribute('data-dark');
    if(document.documentElement.getAttribute('data-theme')==='dark' && dark){
      img.src=dark;
    }
  });
});
</script>

Note: the script above is intentionally small; only swap visible elements and avoid bulk DOM writes.

Efficient JavaScript patterns

When toggling theme, follow patterns that minimize work and avoid forcing synchronous layout calculations.

  • Toggle a single attribute: Set data-theme on document.documentElement rather than toggling many class names. CSS will cascade efficiently.
  • Batch DOM writes: If you must update multiple elements, perform writes separately from reads to avoid layout thrashing.
  • Avoid expensive selectors: Use variables and simple selectors in CSS so repaint cost is lower when theme changes.
  • Debounce non-critical work: If toggling triggers analytics or heavy sync, defer those tasks using requestIdleCallback or setTimeout.
// Efficient toggle example
function setTheme(t){
  document.documentElement.setAttribute('data-theme', t);
  try{ localStorage.setItem('site-theme-preference', t); }catch(e){}
  // defer analytics or heavy tasks
  if('requestIdleCallback' in window){
    requestIdleCallback(function(){ /* send analytics */ });
  } else { setTimeout(function(){ /* send analytics */ }, 200); }
}

Image and media optimizations

Images often dominate bytes on content sites. Optimize them carefully for dark mode:

  • Compress and convert to WebP/AVIF where supported.
  • Use loading="lazy" for below-the-fold images.
  • Prefer CSS filters for minor adjustments rather than loading full alternate assets.
  • Use vector graphics (SVG) for icons and UI elements so they adapt without extra payload.

Testing and measuring performance

Measure before and after changes. Useful tools and metrics:

  • Lighthouse: Check performance score and Core Web Vitals.
  • WebPageTest: Analyze first paint, speed index, and visual progress.
  • Chrome DevTools: Throttle CPU/network to simulate slower devices and observe theme toggling behavior.
  • Real User Monitoring (RUM): Collect data from actual visitors to see real-world impact.

Key metrics to watch when adding dark mode:

  • First Contentful Paint (FCP)
  • Largest Contentful Paint (LCP)
  • Cumulative Layout Shift (CLS)
  • Time to Interactive (TTI)

Quick production checklist

ItemStatus
Inline critical CSS with variables
Tiny head script for fast theme detection
Preload main stylesheet
Use SVGs and CSS for icons
Lazy-load dark-mode-only assets
Batch DOM writes on toggle
Test on slow CPU/network
Monitor RUM metrics post-launch

FAQ

Q: Will dark mode always add extra bytes?

A: Not necessarily. If you use CSS variables and SVGs, dark mode can be mostly zero-byte from an asset perspective. Extra bytes come from alternate images or larger CSS, so optimize to avoid that.

Q: Is inlining CSS safe for caching?

A: Inline critical CSS helps first paint. Keep it minimal; larger CSS should still be cached as external files using preload and proper cache headers.

Q: Should I always swap images for dark mode?

A: Only when necessary. Prefer adaptive SVGs or subtle CSS filters to avoid extra network requests and complexity.

Comments