ves by 85% by leveraging local cache for instant rendering while background sync resolves preference state.
Core Solution
The optimal architecture separates state by volatility and sync requirements. User preferences that define the extension's behavior across installations belong in storage.sync. Ephemeral data, API responses, and large payloads belong in storage.local. The implementation requires explicit quota fallback logic, default value injection, and cross-device change listeners.
Quota Handling & Graceful Fallback
storage.sync enforces strict limits. Always wrap sync writes in try/catch blocks to handle QUOTA_BYTES errors and fall back to local storage without breaking the extension.
// Settings that should follow the user
await browser.storage.sync.set({
temperatureUnit: 'celsius', // °C or °F
timeFormat: '24h', // 12h or 24h
theme: 'dark', // dark or light
defaultLocation: 'London', // weather location
worldClocks: [ // configured clocks
{ timezone: 'America/New_York', label: 'New York' },
{ timezone: 'Asia/Tokyo', label: 'Tokyo' },
]
});
// Cache fetched weather data (don't sync — it's ephemeral)
await browser.storage.local.set({
weatherCache: {
London: {
data: { temp: 15, description: 'Cloudy' },
timestamp: Date.now()
}
}
});
async function saveUserPreferences(prefs) {
try {
await browser.storage.sync.set(prefs);
} catch (error) {
if (error.message.includes('QUOTA_BYTES')) {
console.warn('Sync storage quota exceeded, falling back to local');
await browser.storage.local.set(prefs);
} else {
throw error;
}
}
}
Reading with Defaults
Storage areas are empty on first install. Always pass a defaults object to browser.storage.sync.get() to prevent undefined state propagation.
const DEFAULTS = {
temperatureUnit: 'celsius',
timeFormat: '24h',
theme: 'auto',
defaultLocation: '',
worldClocks: [],
};
async function loadPreferences() {
const stored = await browser.storage.sync.get(DEFAULTS);
// stored will contain stored values OR defaults for missing keys
return stored;
}
Listening for Cross-Device Changes
Firefox Sync propagates changes asynchronously. Attach a listener to storage.onChanged to reactively update the UI when another device modifies synced preferences.
browser.storage.onChanged.addListener((changes, area) => {
if (area !== 'sync') return;
if (changes.theme) {
applyTheme(changes.theme.newValue);
}
if (changes.temperatureUnit) {
refreshWeatherDisplay();
}
if (changes.worldClocks) {
rebuildClockList(changes.worldClocks.newValue);
}
});
Initialization Pattern (Hybrid Strategy)
Load synced preferences for configuration, render instantly from local cache, and trigger background refresh. This pattern eliminates UI blocking while maintaining state accuracy.
const SYNC_KEYS = ['theme', 'temperatureUnit', 'timeFormat', 'defaultLocation', 'worldClocks'];
const LOCAL_KEYS = ['weatherCache', 'lastUpdated'];
// On startup: load sync prefs, use local cache
async function init() {
const prefs = await browser.storage.sync.get(DEFAULTS);
applyPreferences(prefs);
// Try to load cached weather first (instant)
const { weatherCache } = await browser.storage.local.get('weatherCache');
if (weatherCache && isFresh(weatherCache)) {
displayWeather(weatherCache.data);
}
// Then refresh in background
fetchAndCacheWeather(prefs.defaultLocation);
}
Pitfall Guide
- Caching Ephemeral Data in
storage.sync: Storing API responses or time-sensitive data in sync rapidly consumes the 100KB quota and wastes sync bandwidth. These payloads expire quickly and provide no cross-device value. Route all cache/blobs to storage.local.
- Omitting Default Values on Read:
browser.storage.sync.get() returns undefined for missing keys on fresh installs. Failing to pass a defaults object causes downstream logic to crash or render broken UI states. Always provide a complete defaults map.
- Ignoring
storage.onChanged for Synced Areas: Firefox Sync updates propagate asynchronously. Without an onChanged listener, the UI remains stale until a manual refresh or extension reload. Bind reactive update handlers to synced keys to maintain real-time consistency.
- Storing Large Blobs or Base64 in
sync: The 8KB per-item limit is strictly enforced. Base64-encoded images, serialized state trees, or uncompressed JSON will trigger QUOTA_BYTES errors. Compress payloads, split large objects, or store them locally with only reference IDs in sync.
- Explicitly Checking for Firefox Sync Status:
storage.sync is designed to fall back gracefully to local storage when Firefox Sync is disabled or unavailable. Adding conditional checks for sync status introduces unnecessary complexity and race conditions. Trust the API's built-in fallback behavior.
Deliverables
- Hybrid Storage Architecture Blueprint: Systematic key-separation strategy mapping preference volatility to
sync vs local storage areas, including quota-aware write routing and fallback chains.
- Pre-Deployment Storage Validation Checklist:
- Configuration Templates: Ready-to-use
DEFAULTS object structure, SYNC_KEYS/LOCAL_KEYS separation constants, and initialization sequence boilerplate for rapid extension scaffolding.