Media
SVG Embedded Bitmap Analysis

SVG Embedded Bitmap Analysis

Scans all SVG resources on the page — both external files and inline <svg> elements — and flags any that contain embedded bitmap images, reporting name, transfer size, compression encoding, and embedded bitmap details.

What this snippet detects:

CheckScope
Inline base64 bitmaps (PNG, JPEG, WebP, AVIF, GIF…)External SVG files + inline <svg> elements
External bitmap references via <image href="">External SVG files + inline <svg> elements
Transfer sizeExternal SVG files
Compression encoding (gzip, br, zstd, none)External SVG files

Overview

SVG is a vector format designed to scale without quality loss. Embedding raster (bitmap) images inside an SVG — either as base64 data URIs or as external <image> references pointing to bitmap files — introduces several performance problems:

  • Size bloat: Base64 encoding inflates a bitmap by approximately 33%. A 100 KB PNG becomes ~133 KB of inline text, and the wrapping SVG cannot be compressed independently.
  • No format negotiation: The embedded bitmap is served as-is; the browser cannot substitute it with AVIF or WebP based on the Accept header.
  • Broken caching: A change to either the SVG or the bitmap invalidates the other file's cache entry.
  • Blocked parallel loading: The bitmap cannot be fetched in parallel with the SVG — it is hidden inside it and only discovered after the SVG has been downloaded and parsed.

The correct pattern is to reference bitmap assets as separate files:

<!-- ❌ Bitmap baked into SVG — one large, hard-to-optimize resource -->
<img src="infographic.svg" />
<!-- infographic.svg contains: <image href="data:image/png;base64,iVBORw0K…"> -->
 
<!-- ✅ Bitmap served independently — cached and optimised separately -->
<img src="infographic.svg" />
<!-- infographic.svg references: <image href="chart.avif" width="800" height="600"/> -->

Snippet

// SVG Embedded Bitmap Analysis
// https://webperf-snippets.nucliweb.net
 
void (async () => {
  function formatSize(bytes) {
    if (!bytes || bytes === 0) return "—";
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
    return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
  }
 
  function shortName(url) {
    try {
      const path = new URL(url).pathname;
      const name = path.split("/").pop() || path;
      return name.length > 45 ? "…" + name.slice(-42) : name;
    } catch {
      return url.slice(-45);
    }
  }
 
  function compressionFromEntry(entry) {
    if (entry.transferSize === 0 && entry.encodedBodySize === 0) return "cached";
    if (entry.encodedBodySize === 0) return "unknown";
    if (entry.encodedBodySize < entry.decodedBodySize) return "compressed";
    return "none";
  }
 
  function findEmbeddedBitmaps(svgText) {
    const found = [];
 
    // Inline base64-encoded bitmaps via data URIs
    const dataRe = /data:image\/(png|jpe?g|gif|webp|avif|bmp|tiff?|ico);base64,([A-Za-z0-9+/=]*)/gi;
    let m;
    while ((m = dataRe.exec(svgText)) !== null) {
      found.push({
        kind: "inline",
        format: m[1].replace("jpeg", "jpg"),
        estimatedBytes: Math.floor((m[2].length * 3) / 4),
      });
    }
 
    // External bitmap URLs referenced via <image href> or xlink:href
    const hrefRe = /(?:xlink:)?href=["']([^"']*\.(?:png|jpe?g|gif|webp|avif|bmp|tiff?|ico))["']/gi;
    while ((m = hrefRe.exec(svgText)) !== null) {
      found.push({
        kind: "external",
        format: m[1].split(".").pop().toLowerCase().replace("jpeg", "jpg"),
        url: m[1],
      });
    }
 
    return found;
  }
 
  // ── External SVG files (Performance API) ──────────────────────────────
  const svgEntries = performance
    .getEntriesByType("resource")
    .filter((e) => e.name.split("?")[0].toLowerCase().endsWith(".svg"));
 
  const externalResults = await Promise.all(
    svgEntries.map(async (entry) => {
      let compression = compressionFromEntry(entry);
      let bitmaps = [];
 
      try {
        const res = await fetch(entry.name, { cache: "force-cache" });
        const ce = res.headers.get("content-encoding");
        if (ce) compression = ce; // gzip | br | zstd | deflate
        bitmaps = findEmbeddedBitmaps(await res.text());
      } catch {
        // CORS or network error — Performance API data used as fallback
      }
 
      const transferSize = entry.transferSize > 0 ? entry.transferSize : entry.encodedBodySize;
 
      return {
        name: shortName(entry.name),
        url: entry.name,
        transferSize,
        compression,
        bitmaps,
      };
    }),
  );
 
  // ── Inline <svg> elements (DOM scan) ────────────────────────────────
  const inlineSvgs = Array.from(document.querySelectorAll("svg"));
  const inlineSvgTotal = inlineSvgs.length;
  const svgsWithUse = inlineSvgs.filter((svg) => svg.querySelector("use")).length;
  const standaloneInlineSvgs = inlineSvgTotal - svgsWithUse;
 
  const inlineResults = Array.from(document.querySelectorAll("svg"))
    .map((svg, i) => {
      const html = svg.outerHTML;
      const bitmaps = findEmbeddedBitmaps(html);
      if (!bitmaps.length) return null;
      return {
        name: svg.id ? `#${svg.id}` : `inline-svg[${i + 1}]`,
        transferSize: new Blob([html]).size,
        compression: "N/A",
        bitmaps,
      };
    })
    .filter(Boolean);
 
  // ── Output ───────────────────────────────────────────────────────────
  const withBitmaps = [...externalResults.filter((r) => r.bitmaps.length > 0), ...inlineResults];
 
  if (svgEntries.length === 0 && inlineSvgTotal === 0) {
    console.log("No SVG resources found on this page.");
    return;
  }
 
  console.group("%c🖼️ SVG Embedded Bitmap Analysis", "font-weight: bold; font-size: 14px;");
 
  console.log("");
  console.log("%cSummary", "font-weight: bold;");
  console.log(`   External SVG files    : ${svgEntries.length}`);
  const inlineLabel =
    svgsWithUse > 0
      ? `${inlineSvgTotal}  (${svgsWithUse} using <use>, ${standaloneInlineSvgs} standalone)`
      : `${inlineSvgTotal}`;
  console.log(`   Inline <svg> elements : ${inlineLabel}`);
  console.log(
    `   SVGs with bitmaps     : ${withBitmaps.length}${withBitmaps.length > 0 ? "  ⚠️" : "  ✅"}`,
  );
 
  if (externalResults.length > 0) {
    console.log("");
    console.group(`%c📋 External SVG Resources (${externalResults.length})`, "font-weight: bold;");
    console.table(
      externalResults.map((r) => ({
        name: r.name,
        size: formatSize(r.transferSize),
        compression: r.compression,
        "embedded bitmap": r.bitmaps.length ? `⚠️ ${r.bitmaps.length} found` : "✅ none",
      })),
    );
    console.groupEnd();
  }
 
  if (withBitmaps.length > 0) {
    console.log("");
    console.group(
      `%c⚠️ SVGs with Embedded Bitmaps (${withBitmaps.length})`,
      "color: #f59e0b; font-weight: bold;",
    );
 
    withBitmaps.forEach((r) => {
      console.log("");
      console.log(`%c📄 ${r.name}`, "font-weight: bold;");
      console.log(`   SVG size    : ${formatSize(r.transferSize)}`);
      console.log(`   Compression : ${r.compression}`);
      r.bitmaps.forEach((b) => {
        if (b.kind === "inline") {
          console.log(`   🖼️  inline ${b.format.toUpperCase()} — ~${formatSize(b.estimatedBytes)}`);
        } else {
          console.log(`   🔗  external ${b.format.toUpperCase()}${b.url}`);
        }
      });
    });
 
    console.groupEnd();
 
    console.log("");
    console.group("%c💡 Recommendations", "color: #3b82f6; font-weight: bold;");
    console.log("");
    console.log("   Embedded bitmaps inflate SVG size (~33% overhead for base64),");
    console.log("   block format negotiation, and prevent independent caching.");
    console.log("");
    console.log("   ✅ Extract the bitmap and serve it as a separate resource");
    console.log("   ✅ Convert the bitmap to a modern format (AVIF or WebP)");
    console.log('   ✅ Reference it from the SVG: <image href="image.avif" width="…" height="…"/>');
    console.log("   ✅ Both files are then cached and optimised independently");
    console.groupEnd();
  } else {
    console.log("");
    console.log(
      "%c✅ No embedded bitmaps found in SVG resources.",
      "color: #22c55e; font-weight: bold;",
    );
  }
 
  if (standaloneInlineSvgs >= 5) {
    console.log("");
    console.group("%c💡 SVG Sprite Opportunity", "color: #3b82f6; font-weight: bold;");
    console.log("");
    console.log(`   ${standaloneInlineSvgs} standalone inline <svg> elements detected.`);
    console.log("   Each one duplicates markup in the HTML and cannot be cached independently.");
    console.log("   Consider an SVG sprite: define each icon once as a <symbol> and");
    console.log('   reference it anywhere with <use href="#icon-id">.');
    console.log("");
    console.log('%c  <!-- Sprite (once, hidden) -->', "font-family: monospace; color: #6b7280;");
    console.log(
      '%c  <svg hidden>\n    <symbol id="icon-arrow" viewBox="0 0 24 24">…</symbol>\n  </svg>',
      "font-family: monospace;",
    );
    console.log('%c  <!-- Usage (anywhere, N times) -->', "font-family: monospace; color: #6b7280;");
    console.log(
      '%c  <svg aria-hidden="true"><use href="#icon-arrow"/></svg>',
      "font-family: monospace;",
    );
    console.groupEnd();
  }
 
  console.groupEnd();
})();

Understanding the Results

Summary

FieldDescription
External SVG filesCount of SVG resources captured by the Performance API (loaded via <img>, <object>, CSS, fetch, etc.)
Inline <svg> elementsCount of <svg> elements present in the DOM. When some already use the sprite pattern the breakdown shows N using <use>, M standalone
SVGs with bitmapsSVGs (external or inline) that contain at least one embedded bitmap

External SVG Resources Table

ColumnDescription
nameFilename extracted from the resource URL
sizeBytes transferred over the network (compressed). means the entry was served from cache with no transfer
compressionEncoding reported by the Content-Encoding response header. Falls back to comparing encodedBodySize vs decodedBodySize from the Performance API when the header is not accessible
embedded bitmap✅ none — no bitmaps detected · ⚠️ N found — N embedded bitmaps

Compression Values

ValueMeaning
gzipCompressed with GZIP
brCompressed with Brotli
zstdCompressed with Zstandard
noneNo content encoding applied
compressedCompressed (type not accessible — CORS restriction)
cachedResource was served from cache with no transfer size
N/ANot applicable (inline <svg> element — no HTTP transfer)

Embedded Bitmap Types

IconKindDescription
🖼️inlineBase64-encoded bitmap embedded directly as a data:image/…;base64, URI. The reported size is an estimate derived from the base64 string length
🔗externalBitmap file referenced via href or xlink:href on an <image> element. The bitmap is a separate HTTP request, but it is still discovered only after the SVG has loaded

SVG Sprite Opportunity

When 5 or more standalone inline <svg> elements are detected (i.e. not already using <use>), the snippet shows a tip suggesting the SVG sprite pattern.

Inline SVGs repeated across a page inflate HTML size and cannot be cached independently. The sprite pattern defines each icon once as a <symbol> in a hidden <svg> block and references it anywhere with <use href="#id">:

<!-- Define once (hidden, ideally in a separate .svg file loaded via fetch or server-side include) -->
<svg hidden>
  <symbol id="icon-arrow" viewBox="0 0 24 24">…</symbol>
  <symbol id="icon-close" viewBox="0 0 24 24">…</symbol>
</svg>
 
<!-- Use anywhere, any number of times -->
<svg aria-hidden="true"><use href="#icon-arrow"/></svg>
<svg aria-hidden="true"><use href="#icon-close"/></svg>

The summary line for inline <svg> elements shows a breakdown when the sprite pattern is already partially in use:

Inline <svg> elements : 211  (5 using <use>, 206 standalone)

CORS note: The snippet fetches each SVG with cache: "force-cache" to read headers and content without triggering new network requests. Cross-origin SVGs without CORS headers cannot be read — in that case the snippet falls back to Performance API timing data for size and compression, and bitmap detection is skipped for that resource.

Further Reading