DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

Performance Tips for Firefox New Tab Extensions: Sub-100ms Load Times

Performance Tips for Firefox New Tab Extensions: Sub-100ms Load Times

Every time someone opens a new tab, your extension loads. If it's slow, they'll either disable it or dread using it. Here's how to keep load times under 100ms.

Baseline: What Makes a New Tab Feel Fast?

The new tab page replaces Firefox's built-in page, which is basically instant. Users will notice if yours takes more than 200ms. Aim for:

  • First paint: < 50ms
  • Interactive: < 100ms
  • Weather data visible: < 500ms (from cache)

Inline Critical CSS

External stylesheets block rendering. Inline your critical CSS:

<!DOCTYPE html>
<html>
<head>
  <!-- Critical CSS inlined — no blocking request -->
  <style>
    :root { --bg: #fff; --text: #1a1a1a; }
    body { margin: 0; background: var(--bg); color: var(--text); font-family: system-ui; }
    .container { max-width: 800px; margin: 0 auto; padding: 2rem; }
    /* Only above-the-fold styles here */
  </style>
</head>
<body>
  <!-- Content -->
  <script src="app.js" defer></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Use defer for Scripts

<!-- GOOD: script parses after HTML, doesn't block -->
<script src="app.js" defer></script>

<!-- BAD: blocks HTML parsing -->
<script src="app.js"></script>
Enter fullscreen mode Exit fullscreen mode

With defer, the HTML renders before JavaScript runs, so the user sees something immediately.

Apply Theme Before DOM

Flash of wrong theme is jarring:

<head>
  <!-- Run SYNCHRONOUSLY to avoid theme flash -->
  <script>
    // This runs immediately, before any rendering
    (function() {
      const theme = localStorage.getItem('theme') || 'auto';
      const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      const effective = theme === 'auto' ? (prefersDark ? 'dark' : 'light') : theme;
      document.documentElement.setAttribute('data-theme', effective);
    })();
  </script>
  <style>/* ... */</style>
</head>
Enter fullscreen mode Exit fullscreen mode

Yes, this is a synchronous script — but it's tiny and necessary to prevent FOUC.

Load Cached Data First

Don't wait for an API call before rendering:

async function init() {
  // 1. Apply settings from sync storage (fast, local)
  const prefs = await browser.storage.sync.get(DEFAULTS);
  applyPreferences(prefs);

  // 2. Show cached weather immediately (no network needed)
  const { weatherCache } = await browser.storage.local.get('weatherCache');
  if (weatherCache) {
    displayWeather(weatherCache.data);
  } else {
    showWeatherSkeleton();
  }

  // 3. Fetch fresh data in background
  fetchWeatherAndUpdate(prefs.location);

  // 4. Render clocks (pure JS, no async needed)
  initClocks(prefs.worldClocks);
}
Enter fullscreen mode Exit fullscreen mode

With this pattern, the page is visually complete from cached data in < 50ms.

Avoid Layout Thrashing

Batching DOM reads and writes prevents forced reflows:

// BAD: read/write/read/write causes 4 reflows
const w1 = el1.offsetWidth;  // read
el1.style.width = (w1 + 10) + 'px';  // write
const w2 = el2.offsetWidth;  // read (forces reflow)
el2.style.width = (w2 + 10) + 'px';  // write

// GOOD: batch reads, then writes
const w1 = el1.offsetWidth;  // read
const w2 = el2.offsetWidth;  // read (no reflow, still in same layout)
el1.style.width = (w1 + 10) + 'px';  // write
el2.style.width = (w2 + 10) + 'px';  // write
Enter fullscreen mode Exit fullscreen mode

Cache Formatter Objects

Creating Intl.DateTimeFormat objects is expensive. For clocks updating every second:

// Create formatters once, reuse forever
const clockFormatters = new Map();

function getFormatter(timezone) {
  if (!clockFormatters.has(timezone)) {
    clockFormatters.set(timezone, new Intl.DateTimeFormat('en-US', {
      timeZone: timezone,
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
      hour12: false
    }));
  }
  return clockFormatters.get(timezone);
}
Enter fullscreen mode Exit fullscreen mode

requestAnimationFrame for Clock Updates

Use requestAnimationFrame + timestamp diff instead of setInterval to avoid timer drift:

let lastUpdate = 0;

function updateClocks(timestamp) {
  // Only update every ~1 second
  if (timestamp - lastUpdate >= 950) {
    lastUpdate = timestamp;
    renderClocks();
  }
  requestAnimationFrame(updateClocks);
}

requestAnimationFrame(updateClocks);
Enter fullscreen mode Exit fullscreen mode

Minimize Storage Reads

Batch storage reads into one call:

// BAD: multiple awaits, multiple IPC calls
const { theme } = await browser.storage.sync.get('theme');
const { location } = await browser.storage.sync.get('location');
const { clocks } = await browser.storage.sync.get('clocks');

// GOOD: one IPC call
const { theme, location, clocks } = await browser.storage.sync.get(['theme', 'location', 'clocks']);
Enter fullscreen mode Exit fullscreen mode

Measuring Performance

// Measure your init time
const t0 = performance.now();
await init();
const t1 = performance.now();
console.log(`Init took ${t1 - t0}ms`);
Enter fullscreen mode Exit fullscreen mode

Use Firefox's built-in Performance profiler (F12 → Performance tab) to identify bottlenecks.

Results

With these techniques, Weather & Clock Dashboard achieves:

  • First paint: ~20ms (cached theme applied synchronously)
  • Cached weather displayed: ~40ms
  • Clock rendered: ~45ms
  • Fresh weather visible: ~400ms (network permitting)

Weather & Clock Dashboard — free Firefox new tab with weather, world clocks, and search. MIT licensed.

Top comments (0)