Chrome Extension Lazy Loading Content Scripts — Best Practices

4 min read

Lazy Loading Content Scripts

On-demand and conditional content script injection for Chrome Extensions (MV3).

Static vs Dynamic

Static (manifest.json) - Always injected:

{ "content_scripts": [{ "matches": ["<all_urls>"], "js": ["content.js"] }] }

Dynamic (chrome.scripting.executeScript) - On-demand:

chrome.action.onClicked.addListener(async (tab) => {
  await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ['content.js']
  });
});

When to Lazy Load

// Context menu chrome.contextMenus.create({ id: ‘analyze’, title: ‘Analyze’, contexts: [‘page’] }); chrome.contextMenus.onClicked.addListener(async (info, tab) => { if (info.menuItemId === ‘analyze’) { await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: [‘analyzer.js’] }); } });

## World: ISOLATED vs MAIN {#world-isolated-vs-main}
```javascript
// ISOLATED (default) - sandboxed
await chrome.scripting.executeScript({
  target: { tabId: tab.id }, world: 'ISOLATED',
  func: () => { /* cannot access page vars */ }
});
// MAIN - page context
await chrome.scripting.executeScript({
  target: { tabId: tab.id }, world: 'MAIN',
  func: () => window.pageVar
});

Injection Targets

// All frames
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] });
// Specific frame
await chrome.scripting.executeScript({ target: { tabId: tab.id, frameIds: [frameId] }, files: ['content.js'] });

Check If Injected

async function injectIfNeeded(tabId) {
  const results = await chrome.scripting.executeScript({
    target: { tabId },
    func: () => window.__EXTENSION_INJECTED__
  });
  if (!results[0]?.result) {
    await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] });
  }
}

Bootstrap + Lazy Load

// bootstrap.js (static)
document.addEventListener('click', async (e) => {
  if (e.target.matches('[data-lazy]')) {
    await chrome.runtime.sendMessage({ type: 'LOAD_FEATURE' });
  }
});
// background.js
chrome.runtime.onMessage.addListener((msg, sender) => {
  if (msg.type === 'LOAD_FEATURE' && sender.tab) {
    chrome.scripting.executeScript({
      target: { tabId: sender.tab.id },
      files: ['full-feature.js']
    });
  }
});

CSS Injection

await chrome.scripting.insertCSS({ target: { tabId }, files: ['style.css'] });
await chrome.scripting.removeCSS({ target: { tabId }, files: ['style.css'] });

Error Handling

try {
  await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] });
} catch (error) {
  if (error.message.includes('Cannot access contents')) console.log('Restricted page');
  else if (error.message.includes('No tab with id')) console.log('Tab closed');
}

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.