iToverDose/Software· 7 MAY 2026 · 12:04

Building a Chrome extension? Learn how to extract hidden images efficiently

A developer shares hard-won lessons from building an image downloader extension, revealing how to handle Shadow DOM, CSS backgrounds, and Manifest V3 limitations without freezing the browser.

DEV Community5 min read0 Comments

Three years of manually saving images led one developer to frustration. The "Save image as" option often failed, returning blank placeholders or low-resolution thumbnails. Popular extensions missed half the images and included unwanted analytics. Frustrated by these limitations, the developer built their own solution—Image Harvest—a Chrome extension designed to capture every image on a webpage efficiently.

The project uncovered critical challenges in modern web scraping, particularly around Shadow DOM, CSS background images, and the constraints of Chrome’s Manifest V3. Through trial and error, the developer discovered practical solutions to these problems, which are now shared in this technical breakdown.

The five locations where images hide on modern websites

A single call to document.querySelectorAll('img') only captures about half of the images on a typical 2026 webpage. The remaining images are scattered across five common locations:

  • `<picture>` elements and `srcset` attributes: Many sites provide multiple image resolutions, but browsers select only one. Extracting all variants ensures you don’t miss higher-quality versions.
  • CSS `background-image` properties: Common in hero sections, gallery layouts, and e-commerce product displays, these images are often overlooked by basic image scrapers.
  • Lazy-loaded images: Triggered by loading="lazy", IntersectionObserver, or framework-specific mechanisms, these images load dynamically as the user scrolls.
  • Inline `<svg>` content: Embedded SVGs can contain raster images or references, which may be essential to capture.
  • Shadow DOM: Web components and frameworks like React frequently use Shadow DOM, creating isolated DOM trees where images reside beyond the reach of standard selectors.

To build a comprehensive image downloader, your code must account for all five locations. The following function demonstrates a robust approach to scanning an entire document:

function extractImagesFromDocument(doc) {
  const images = new Set();

  // 1. Plain <img> and <picture> elements
  doc.querySelectorAll('img').forEach(img => {
    if (img.currentSrc) images.add(img.currentSrc);
    if (img.src) images.add(img.src);
    if (img.srcset) {
      parseSrcset(img.srcset).forEach(url => images.add(url));
    }
  });

  // 2. CSS background-image on every element
  doc.querySelectorAll('*').forEach(el => {
    const bg = getComputedStyle(el).backgroundImage;
    if (bg && bg !== 'none') {
      const matches = bg.match(/url\(["']?(.*?)["']?\)/g) || [];
      matches.forEach(m => {
        const url = m.replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
        if (url && !url.startsWith('data:')) images.add(url);
      });
    }
  });

  // 3. Recurse into open Shadow DOM
  doc.querySelectorAll('*').forEach(el => {
    if (el.shadowRoot) {
      extractImagesFromDocument(el.shadowRoot).forEach(url => images.add(url));
    }
  });

  // 4. Same-origin iframes
  doc.querySelectorAll('iframe').forEach(iframe => {
    try {
      if (iframe.contentDocument) {
        extractImagesFromDocument(iframe.contentDocument).forEach(url => images.add(url));
      }
    } catch (e) {
      // Cross-origin iframes are silently skipped
    }
  });

  return images;
}

Several nuances make this process more complex than it appears. First, getComputedStyle is resource-intensive. On a page with 5,000 nodes—like Pinterest—calling it on every element can consume 200 milliseconds. To avoid UI freezing, the developer implemented batching using requestIdleCallback, streaming results to the side panel as they become available.

Second, document.querySelectorAll('*') remains efficient even on large pages because it performs a single traversal. The real cost lies in the processing that follows. Finally, closed Shadow DOM is intentionally unreachable. There is no workaround for this limitation, so developers must accept partial results and adapt their strategies accordingly.

Avoiding browser freezes with perceptual hashing in Web Workers

After extraction, a single webpage can yield over 200 images, many of which are duplicates at different resolutions. Comparing images pixel-by-pixel is impractical due to resizing differences. Instead, perceptual hashing provides a reliable way to identify similar images without exhaustive analysis.

The developer chose dHash (difference hash) for its balance of simplicity and effectiveness. Unlike more complex algorithms like pHash, dHash generates a compact hash by comparing adjacent pixels in a downscaled image. Here’s how it works:

async function dHash(imageUrl) {
  const img = await loadImage(imageUrl);
  const canvas = new OffscreenCanvas(9, 8);
  const ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0, 9, 8);
  const { data } = ctx.getImageData(0, 0, 9, 8);

  let hash = 0n;
  for (let row = 0; row < 8; row++) {
    for (let col = 0; col < 8; col++) {
      const left = grayscale(data, row * 9 + col);
      const right = grayscale(data, row * 9 + col + 1);
      hash = (hash << 1n) | (left > right ? 1n : 0n);
    }
  }
  return hash;
}

function grayscale(data, pixelIndex) {
  const i = pixelIndex * 4;
  return (data[i] + data[i + 1] + data[i + 2]) / 3;
}

function hammingDistance(a, b) {
  let xor = a ^ b;
  let dist = 0;
  while (xor) {
    dist += Number(xor & 1n);
    xor >>= 1n;
  }
  return dist;
}

Two images with a Hamming distance below 8 are considered similar enough for deduplication. On average hardware, this process takes about 50 milliseconds per image. Crucially, the developer ran this computation in a Web Worker to prevent UI freezing. The worker-based pipeline streams results back to the side panel as they’re computed:

// In the panel UI:
const worker = new Worker('hash-worker.js');
worker.postMessage({ urls: extractedImages });
worker.onmessage = (e) => {
  updateUIWithCluster(e.data);
};

The use of OffscreenCanvas inside the worker eliminates the need for round trips to the main thread, enabling smooth performance even during intensive processing.

Manifest V3’s service worker pitfalls and how to bypass them

Chrome’s Manifest V3 introduced significant changes, including the replacement of persistent background pages with short-lived service workers. These workers terminate after approximately 30 seconds of inactivity, leading to frustrating cold starts when reopening an extension panel. On slower machines, this cold start can introduce a noticeable delay of up to 150 milliseconds.

The solution lies in leveraging chrome.storage.session, a storage API designed for ephemeral data that persists only for the browser session. The developer implemented a caching strategy to store extraction results and serve them instantly on subsequent panel openings:

// In the service worker:
async function getCachedExtraction(tabId) {
  const cache = await chrome.storage.session.get(`extraction_${tabId}`);
  return cache[`extraction_${tabId}`];
}

async function setCachedExtraction(tabId, results) {
  await chrome.storage.session.set({
    [`extraction_${tabId}`]: {
      timestamp: Date.now(),
      results
    }
  });
}

// In the panel:
async function loadPanel(tabId) {
  const cached = await getCachedExtraction(tabId);
  if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
    renderResults(cached.results); // Instant load
    return;
  }

  showLoading();
  const fresh = await runExtraction(tabId);
  renderResults(fresh);
}

This approach ensures users experience near-instant results when revisiting the extension within a five-minute window, effectively mitigating the cold-start delays inherent in Manifest V3.

The lessons from this project extend beyond image extraction. Developers building Chrome extensions in the Manifest V3 era must account for service worker lifecycles, optimize performance with Web Workers, and account for the fragmented nature of modern web content. By addressing these challenges proactively, tools like Image Harvest demonstrate how thoughtful engineering can overcome the limitations of today’s web platform.

AI summary

Learn how to build a Chrome extension that captures every image on a page, including Shadow DOM and CSS backgrounds, while optimizing for Manifest V3 and performance.

Comments

00
LEAVE A COMMENT
ID #KDDNUT

0 / 1200 CHARACTERS

Human check

9 + 7 = ?

Will appear after editor review

Moderation · Spam protection active

No approved comments yet. Be first.