SVG embedded bitmap analysis
Chrome 54+ Edge 17+ Firefox 45+ Safari 16.4+ Details
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 |
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
Run it from an AI agent: the prompt uses the CLI to measure any URL and report the result.
// SVG Embedded Bitmap Analysis
// https://webperf-snippets.nucliweb.net
(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) {
// Cross-origin resources without Timing-Allow-Origin report every size as 0
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 {
script: "SVG-Embedded-Bitmap-Analysis",
status: "ok",
count: 0,
details: { externalSvgCount: 0, inlineSvgTotal: 0, svgsWithUse: 0, standaloneInlineSvgs: 0, spriteOpportunity: false },
items: [],
issues: [],
};
}
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();
return {
script: "SVG-Embedded-Bitmap-Analysis",
status: "ok",
count: withBitmaps.length,
details: {
externalSvgCount: svgEntries.length,
inlineSvgTotal,
svgsWithUse,
standaloneInlineSvgs,
// Five or more standalone inline SVGs are worth moving into a <symbol> sprite
spriteOpportunity: standaloneInlineSvgs >= 5,
},
items: withBitmaps.map((r) => ({
url: r.url || r.name,
name: r.name,
hasBitmap: true,
bitmapCount: r.bitmaps.length,
bitmapTypes: r.bitmaps.map((b) => b.format).join(", "),
sizeBytes: r.transferSize,
})),
issues: withBitmaps.length > 0
? [{ severity: "warning", message: `${withBitmaps.length} SVG file(s) contain embedded bitmaps` }]
: [],
};
})();
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) |
unknown | Size not accessible (cross-origin without Timing-Allow-Origin) |
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.
Browser support
| Feature | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| PerformanceResourceTiming.transferSize (opens in a new tab) | 54 | 17 | 45 | 16.4 |
| fetch (opens in a new tab) | 42 | 14 | 39 | 10.1 |
| All of the above | 54 | 17 | 45 | 16.4 |
Source: MDN browser compatibility data (opens in a new tab), version 8.1.4.
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