Chrome Storage API Overview

19 min read

Chrome Storage API Overview

The Chrome Storage API provides reliable data persistence for Chrome extensions. Unlike localStorage, it offers better performance, works across browser sessions, and can sync data across devices when users are signed in to Chrome. This comprehensive guide will help you master data storage for your extensions.

Why Not localStorage?

While localStorage works in extensions, the Chrome Storage API is specifically designed for extension needs:

  • Persistence across browser restarts - Data survives Chrome closes and system reboots
  • Sync support - Automatically syncs data across devices when users sign in to Chrome
  • Larger storage quota - More space than localStorage’s typically 5MB limit
  • Asynchronous API - Doesn’t block the UI thread like localStorage can
  • Service Worker compatible - Works with Manifest V3 service workers

Comparison Table

Feature Chrome Storage localStorage
Async Yes No
Sync across devices Yes (sync type) No
Quota ~100KB sync, ~5MB local ~5MB
Accessible from service worker Yes No
Event listeners Yes (onChanged) No

Storage Types

Chrome provides three storage areas, each with different use cases:

sync Storage

Data syncs across all devices where the user is signed in. Perfect for user preferences that should follow the user across devices.

chrome.storage.sync.set({key: 'value'}, () => {
  console.log('Data saved to sync storage');
});

chrome.storage.sync.get(['key'], (result) => {
  console.log('Retrieved:', result.key);
});

// Get all data
chrome.storage.sync.get(null, (result) => {
  console.log('All sync data:', result);

});

Quota: Approximately 100KB total, 8KB per item recommended for optimal sync performance.

Best for: User preferences, settings, themes, small amounts of user data

local Storage

Data stays on the current device only. Use for large data that shouldn’t sync or data that doesn’t need to follow the user.

chrome.storage.local.set({key: 'value'}, () => {
  console.log('Data saved to local storage');
});

chrome.storage.local.get(['key'], (result) => {
  console.log('Retrieved:', result.key);
});

// Get all local data
chrome.storage.local.get(null, (result) => {
  console.log('All local data:', result);

});

Quota: Typically around 5MB total.

Best for: Cached data, large datasets, device-specific settings

managed Storage

Storage controlled by enterprise policies (read-only for extensions). Administrators set this up through Chrome enterprise policies.

chrome.storage.managed.get(['policyKey'], (result) => {
  console.log('Policy value:', result.policyKey);
});

// Get all managed policies
chrome.storage.managed.get(null, (result) => {
  console.log('All policies:', result);

});

No quota limits - Determined by enterprise policy.

Best for: Enforced settings in enterprise environments

Advanced Usage

Storing Complex Data

You can store objects, arrays, and complex data structures:

const userSettings = {
  theme: 'dark',
  notifications: true,
  language: 'en',
  recentFiles: [
    { name: 'document.pdf', accessed: Date.now() },
    { name: 'notes.txt', accessed: Date.now() }
  ],
  shortcuts: {
    save: 'Ctrl+S',
    open: 'Ctrl+O',
    close: 'Ctrl+W'

  }
};

chrome.storage.sync.set({ settings: userSettings }, () => {
  console.log('Complex data saved');
});

// Retrieve nested data
chrome.storage.sync.get(['settings'], (result) => {
  if (result.settings) {
    console.log('Theme:', result.settings.theme);
    console.log('Recent files:', result.settings.recentFiles);
  }

});

Handling Async Operations Properly

The storage API uses callbacks, but you can wrap it in promises for cleaner async/await code:

const storage = {
  get: (keys) => new Promise((resolve) => {
    chrome.storage.sync.get(keys, resolve);
  }),
  set: (items) => new Promise((resolve) => {
    chrome.storage.sync.set(items, resolve);
  }),
  remove: (keys) => new Promise((resolve) => {
    chrome.storage.sync.remove(keys, resolve);
  }),
  clear: () => new Promise((resolve) => {
    chrome.storage.sync.clear(resolve);
  })
};

// Usage with async/await
async function handleData() {
  try {
    const result = await storage.get('settings');
    await storage.set({ 
      settings: { 
        ...result.settings, 
        theme: 'light' 
      } 
    });
    await storage.remove('oldKey');
    console.log('Operations completed');
  } catch (error) {
    console.error('Storage error:', error);
  }
}

// Alternative: Use browser.storage (Firefox support)
async function modernStorage() {
  const result = await browser.storage.sync.get('key');
  await browser.storage.sync.set({ key: 'value' });

}

Listening for Changes

You can monitor storage changes across all contexts:

chrome.storage.onChanged.addListener((changes, area) => {
  console.log('Storage changed in:', area);
  
  if (area === 'sync' && changes.settings) {
    const oldValue = changes.settings.oldValue;
    const newValue = changes.settings.newValue;
    console.log('Settings changed:', { oldValue, newValue });
    
    // Update UI accordingly
    applyTheme(newValue.theme);
  }
  
  if (area === 'local' && changes.cachedData) {
    console.log('Cache updated:', changes.cachedData.newValue);

  }
});

Storage Quotas and Limits

Be mindful of storage limits to prevent errors:

Storage Type Total Limit Per-Item Limit Recommended Per-Item
sync ~100KB 8KB ~8KB
local ~5MB None N/A
managed No limit No limit N/A

Estimating Storage Usage

function estimateStorageUsage() {
  // Check sync storage
  chrome.storage.sync.getBytesInUse(null, (bytes) => {
    console.log(`Using ${bytes} bytes of sync storage`);
    console.log(`Approximate items remaining: ${100000 - bytes}`);
  });
  
  // Check specific keys
  chrome.storage.sync.getBytesInUse(['settings', 'cache'], (bytes) => {
    console.log(`Settings and cache use ${bytes} bytes`);
  });
  
  // Check local storage
  chrome.storage.local.getBytesInUse(null, (bytes) => {
    console.log(`Using ${bytes} bytes of local storage`);
  });
  
  // Check specific keys
  chrome.storage.sync.getBytesInUse(['settings', 'cache'], (bytes) => {
    console.log(`Settings and cache: ${bytes} bytes`);
  });
}

Handling Quota Errors

async function saveLargeData(data) {
  try {
    await chrome.storage.sync.set({ largeData: data });
  } catch (error) {
    if (error.message.includes('QUOTA_BYTES')) {
      // Fallback to local storage
      await chrome.storage.local.set({ largeData: data });
      console.log('Saved to local storage instead');
    } else {
      throw error;
    }
  }
}

Handling Quota Exceeded

chrome.storage.sync.set({ largeData: bigObject })
  .then(() => console.log('Saved successfully'))
  .catch((error) => {
    if (error.message.includes('QUOTA_BYTES')) {
      console.error('Storage quota exceeded');
      // Fallback to local storage
      chrome.storage.local.set({ largeData: bigObject });
    }
  });

Best Practices

Handle Errors Gracefully

chrome.storage.sync.set({ key: value })
  .then(() => {
    console.log('Saved successfully');
  })
  .catch((error) => {
    console.error('Storage error:', error);
    // Fallback to local storage
    chrome.storage.local.set({ key: value })
      .then(() => console.log('Saved to local fallback'))
      .catch(err => console.error('Local storage also failed:', err));
  });

// Also handle callback errors (Manifest V2 compatibility)
chrome.storage.sync.set({ key: value }, () => {
  if (chrome.runtime.lastError) {
    console.error('Error:', chrome.runtime.lastError.message);
  } else {
    console.log('Saved successfully');
  }
});

Don’t Store Sensitive Data Unencrypted

The storage API doesn’t encrypt by default. For sensitive data like API keys or personal information, use encryption:

// Use the Web Crypto API for encryption
async function encryptAndStore(data, key) {
  const encoded = new TextEncoder().encode(JSON.stringify(data));
  
  // Generate a key from user's password or use a stored key
  const cryptoKey = await crypto.subtle.generateKey(

    { name: 'AES-GCM' },
    true,
    ['encrypt', 'decrypt']
  );
  

  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encrypted = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    cryptoKey,
    encoded
  );
  
  // Store encrypted data
  await chrome.storage.sync.set({
    secureData: Array.from(new Uint8Array(encrypted)),
    iv: Array.from(iv)
  });
  
  // Store the key separately (in local storage or managed)
  return cryptoKey;
}

// Decrypt when needed
async function decryptAndRetrieve() {
  const { secureData, iv } = await chrome.storage.sync.get(['secureData', 'iv']);
  
  const decrypted = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv: new Uint8Array(iv) },
    cryptoKey,
    new Uint8Array(secureData)
  );
  
  return JSON.parse(new TextDecoder().decode(decrypted));
}

// Decrypting data
async function decryptData(encryptedData, iv) {
  const key = await crypto.subtle.importKey(
    'raw',
    await getKeyMaterial(),
    { name: 'PBKDF2' },
    false,
    ['deriveBits', 'deriveKey']
  );
  
  const decrypted = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv: new Uint8Array(iv) },
    key,
    new Uint8Array(encryptedData)
  );
  
  return JSON.parse(new TextDecoder().decode(decrypted));
}

Optimize for Sync

Keep sync storage efficient:

  • Store only user preferences, not cached data
  • Use meaningful prefixes for organized keys
  • Remove unused data promptly
  • Compress data if approaching limits
// Good key naming convention
const KEYS = {
  SETTINGS: 'user_settings',
  THEME: 'user_theme',
  LAST_SYNC: 'sync_lastTimestamp',
  BOOKMARKS: 'cache_bookmarks'  // Note: cache prefix indicates it's cache
};

// Bad - hard to manage
chrome.storage.sync.set({
  a: value1,  // Unclear purpose
  b: value2,  // What is this?
  c: value3   // Hard to find and manage
});

Data Migration

When updating your extension, you might need to migrate data:

chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === 'update') {
    // Migrate data from old keys to new keys
    migrateData();
  }
});

async function migrateData() {
  const oldData = await chrome.storage.local.get('oldKeyName');
  
  if (oldData.oldKeyName) {
    // Transform and save to new location
    await chrome.storage.sync.set({
      newKeyName: transformData(oldData.oldKeyName)
    });
    
    // Remove old data
    await chrome.storage.local.remove('oldKeyName');
    
    console.log('Data migration completed');
  }
}

function transformData(data) {
  // Transform old data format to new format
  return {
    ...data,
    migrated: true,
    migratedAt: Date.now()
  };
}

Comparing Storage Options

Feature sync local localStorage
Persists restart Yes Yes Yes
Syncs across devices Yes No No
Storage quota ~100KB ~5MB ~5MB
Async Yes Yes No
Event listeners Yes Yes No
Service worker ready Yes Yes No
Enterprise ready Yes Yes No

Real-World Example: User Preferences Manager

// preferences.js - A complete preferences manager


class PreferencesManager {
  constructor() {
    this.defaults = {
      theme: 'light',
      notifications: true,
      language: 'en',
      autoSave: true,
      compactMode: false
    };
  }
  
  async init() {
    // Load saved preferences or use defaults
    const saved = await this.getAll();
    this.preferences = { ...this.defaults, ...saved };
    
    // Listen for changes from other contexts
    chrome.storage.onChanged.addListener((changes, area) => {
      if (area === 'sync') {
        this.handleChanges(changes);
      }
    });
    
    return this.preferences;
  }
  

  async get(key) {
    const result = await chrome.storage.sync.get(key);
    return result[key] ?? this.defaults[key];
  }
  
  async set(key, value) {
    await chrome.storage.sync.set({ [key]: value });
    this.preferences[key] = value;
  }
  
  async getAll() {
    return await chrome.storage.sync.get(null);

  }
  
  async reset() {
    await chrome.storage.sync.clear();
    this.preferences = { ...this.defaults };
  }
  
  handleChanges(changes) {
    Object.keys(changes).forEach(key => {
      this.preferences[key] = changes[key].newValue;
    });
  }
}

// Usage
const prefs = new PreferencesManager();
prefs.init().then(() => {
  console.log('Preferences loaded:', prefs.preferences);
});

Conclusion

The Chrome Storage API is the recommended way to store user preferences and data in your extension. Its sync capabilities, larger quotas, and asynchronous design make it superior to localStorage for most extension use cases.

Remember these key best practices:

  • Handle errors gracefully with fallbacks
  • Consider encryption for sensitive data
  • Optimize for sync with appropriate data size
  • Use meaningful key names
  • Listen for changes to keep UI in sync

With these techniques, you can build robust, reliable data storage into your Chrome extensions!

=======

Storage Migration Strategies

When moving from localStorage or other storage solutions to chrome.storage, plan your migration carefully to preserve user data.

Create a migration function that runs on extension startup. Check for old data in localStorage and transfer it to chrome.storage. After successful migration, clear the old localStorage to prevent duplicate processing.

Version your storage schema to handle future migrations. Store a version number with your data, allowing you to upgrade schemas when your extension evolves. This approach prevents compatibility issues as your extension grows.

Debugging Storage Issues

Storage-related bugs can be challenging to diagnose. Chrome provides built-in tools for inspecting stored data.

Navigate to chrome://extensions and click the “Service Worker” link for your extension. In the Console, type chrome.storage to access the Storage Area panel. This shows all stored keys and values.

Use the Application tab in DevTools to inspect localStorage and sessionStorage if you’re still using them. Check for size limits being exceeded, which can cause writes to fail silently.

Encryption Considerations

While chrome.storage provides some protection, sensitive data may require additional encryption. Chrome provides the identity API for securing user credentials.

For highly sensitive data, consider using the Web Crypto API to encrypt data before storing. This provides defense in depth, protecting data even if the device is compromised.

Be cautious about storing tokens or credentials. Consider using the chrome.identity API for OAuth flows rather than storing tokens directly. This provides secure token management without manual encryption implementation.

quality/expand-thin-a1-r5

No previous article
No next article