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>
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>
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>
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);
}
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
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);
}
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);
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']);
Measuring Performance
// Measure your init time
const t0 = performance.now();
await init();
const t1 = performance.now();
console.log(`Init took ${t1 - t0}ms`);
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)