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:
| Check | Scope |
|---|---|
| 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 size | External 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
Acceptheader. - 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
| Field | Description |
|---|---|
| External SVG files | Count of SVG resources captured by the Performance API (loaded via <img>, <object>, CSS, fetch, etc.) |
Inline <svg> elements | Count of <svg> elements present in the DOM. When some already use the sprite pattern the breakdown shows N using <use>, M standalone |
| SVGs with bitmaps | SVGs (external or inline) that contain at least one embedded bitmap |
External SVG Resources Table
| Column | Description |
|---|---|
name | Filename extracted from the resource URL |
size | Bytes transferred over the network (compressed). — means the entry was served from cache with no transfer |
compression | Encoding 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
| Value | Meaning |
|---|---|
gzip | Compressed with GZIP |
br | Compressed with Brotli |
zstd | Compressed with Zstandard |
none | No content encoding applied |
compressed | Compressed (type not accessible — CORS restriction) |
cached | Resource was served from cache with no transfer size |
N/A | Not applicable (inline <svg> element — no HTTP transfer) |
Embedded Bitmap Types
| Icon | Kind | Description |
|---|---|---|
| 🖼️ | inline | Base64-encoded bitmap embedded directly as a data:image/…;base64, URI. The reported size is an estimate derived from the base64 string length |
| 🔗 | external | Bitmap 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
- SVG
<image>element (opens in a new tab) | MDN - SVG
<use>element (opens in a new tab) | MDN - Data URLs (opens in a new tab) | MDN
- SVGOMG (opens in a new tab) | jakearchibald.github.io
- SVGO (opens in a new tab) | GitHub
- Image Element Audit | webperf-snippets