Skip to content

Commit 01babcb

Browse files
committed
Add SVG Embedded Bitmap Analysis snippet
Detects bitmap images embedded inside SVG resources β€” both inline base64 data URIs and external <image> references β€” reporting name, transfer size, compression encoding, and embedded bitmap details.
1 parent 357d1a5 commit 01babcb

2 files changed

Lines changed: 267 additions & 0 deletions

File tree

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
# SVG Embedded Bitmap Analysis
2+
3+
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.
4+
5+
**What this snippet detects:**
6+
7+
| Check | Scope |
8+
| --------------------------------------------------- | -------------------------------------------- |
9+
| Inline base64 bitmaps (PNG, JPEG, WebP, AVIF, GIF…) | External SVG files + inline `<svg>` elements |
10+
| External bitmap references via `<image href="">` | External SVG files + inline `<svg>` elements |
11+
| Transfer size | External SVG files |
12+
| Compression encoding (gzip, br, zstd, none) | External SVG files |
13+
14+
### Overview
15+
16+
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:
17+
18+
- **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.
19+
- **No format negotiation**: The embedded bitmap is served as-is; the browser cannot substitute it with AVIF or WebP based on the `Accept` header.
20+
- **Broken caching**: A change to either the SVG or the bitmap invalidates the other file's cache entry.
21+
- **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.
22+
23+
The correct pattern is to reference bitmap assets as separate files:
24+
25+
```html
26+
<!-- ❌ Bitmap baked into SVG β€” one large, hard-to-optimize resource -->
27+
<img src="infographic.svg" />
28+
<!-- infographic.svg contains: <image href="data:image/png;base64,iVBORw0K…"> -->
29+
30+
<!-- βœ… Bitmap served independently β€” cached and optimised separately -->
31+
<img src="infographic.svg" />
32+
<!-- infographic.svg references: <image href="chart.avif" width="800" height="600"/> -->
33+
```
34+
35+
### Snippet
36+
37+
```js copy
38+
// SVG Embedded Bitmap Analysis
39+
// https://webperf-snippets.nucliweb.net
40+
41+
(async () => {
42+
function formatSize(bytes) {
43+
if (!bytes || bytes === 0) return "β€”";
44+
if (bytes < 1024) return `${bytes} B`;
45+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
46+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
47+
}
48+
49+
function shortName(url) {
50+
try {
51+
const path = new URL(url).pathname;
52+
const name = path.split("/").pop() || path;
53+
return name.length > 45 ? "…" + name.slice(-42) : name;
54+
} catch {
55+
return url.slice(-45);
56+
}
57+
}
58+
59+
function compressionFromEntry(entry) {
60+
if (entry.transferSize === 0 && entry.encodedBodySize === 0) return "cached";
61+
if (entry.encodedBodySize === 0) return "unknown";
62+
if (entry.encodedBodySize < entry.decodedBodySize) return "compressed";
63+
return "none";
64+
}
65+
66+
function findEmbeddedBitmaps(svgText) {
67+
const found = [];
68+
69+
// Inline base64-encoded bitmaps via data URIs
70+
const dataRe = /data:image\/(png|jpe?g|gif|webp|avif|bmp|tiff?|ico);base64,([A-Za-z0-9+/=]*)/gi;
71+
let m;
72+
while ((m = dataRe.exec(svgText)) !== null) {
73+
found.push({
74+
kind: "inline",
75+
format: m[1].replace("jpeg", "jpg"),
76+
estimatedBytes: Math.floor((m[2].length * 3) / 4),
77+
});
78+
}
79+
80+
// External bitmap URLs referenced via <image href> or xlink:href
81+
const hrefRe = /(?:xlink:)?href=["']([^"']*\.(?:png|jpe?g|gif|webp|avif|bmp|tiff?|ico))["']/gi;
82+
while ((m = hrefRe.exec(svgText)) !== null) {
83+
found.push({
84+
kind: "external",
85+
format: m[1].split(".").pop().toLowerCase().replace("jpeg", "jpg"),
86+
url: m[1],
87+
});
88+
}
89+
90+
return found;
91+
}
92+
93+
// ── External SVG files (Performance API) ──────────────────────────────
94+
const svgEntries = performance
95+
.getEntriesByType("resource")
96+
.filter((e) => e.name.split("?")[0].toLowerCase().endsWith(".svg"));
97+
98+
const externalResults = await Promise.all(
99+
svgEntries.map(async (entry) => {
100+
let compression = compressionFromEntry(entry);
101+
let bitmaps = [];
102+
103+
try {
104+
const res = await fetch(entry.name, { cache: "force-cache" });
105+
const ce = res.headers.get("content-encoding");
106+
if (ce) compression = ce; // gzip | br | zstd | deflate
107+
bitmaps = findEmbeddedBitmaps(await res.text());
108+
} catch {
109+
// CORS or network error β€” Performance API data used as fallback
110+
}
111+
112+
const transferSize = entry.transferSize > 0 ? entry.transferSize : entry.encodedBodySize;
113+
114+
return {
115+
name: shortName(entry.name),
116+
url: entry.name,
117+
transferSize,
118+
compression,
119+
bitmaps,
120+
};
121+
}),
122+
);
123+
124+
// ── Inline <svg> elements (DOM scan) ────────────────────────────────
125+
const inlineSvgTotal = document.querySelectorAll("svg").length;
126+
const inlineResults = Array.from(document.querySelectorAll("svg"))
127+
.map((svg, i) => {
128+
const html = svg.outerHTML;
129+
const bitmaps = findEmbeddedBitmaps(html);
130+
if (!bitmaps.length) return null;
131+
return {
132+
name: svg.id ? `#${svg.id}` : `inline-svg[${i + 1}]`,
133+
transferSize: new Blob([html]).size,
134+
compression: "N/A",
135+
bitmaps,
136+
};
137+
})
138+
.filter(Boolean);
139+
140+
// ── Output ───────────────────────────────────────────────────────────
141+
const withBitmaps = [...externalResults.filter((r) => r.bitmaps.length > 0), ...inlineResults];
142+
143+
if (svgEntries.length === 0 && inlineSvgTotal === 0) {
144+
console.log("No SVG resources found on this page.");
145+
return;
146+
}
147+
148+
console.group("%cπŸ–ΌοΈ SVG Embedded Bitmap Analysis", "font-weight: bold; font-size: 14px;");
149+
150+
console.log("");
151+
console.log("%cSummary", "font-weight: bold;");
152+
console.log(` External SVG files : ${svgEntries.length}`);
153+
console.log(` Inline <svg> elements : ${inlineSvgTotal}`);
154+
console.log(
155+
` SVGs with bitmaps : ${withBitmaps.length}${withBitmaps.length > 0 ? " ⚠️" : " βœ…"}`,
156+
);
157+
158+
if (externalResults.length > 0) {
159+
console.log("");
160+
console.group(`%cπŸ“‹ External SVG Resources (${externalResults.length})`, "font-weight: bold;");
161+
console.table(
162+
externalResults.map((r) => ({
163+
name: r.name,
164+
size: formatSize(r.transferSize),
165+
compression: r.compression,
166+
"embedded bitmap": r.bitmaps.length ? `⚠️ ${r.bitmaps.length} found` : "βœ… none",
167+
})),
168+
);
169+
console.groupEnd();
170+
}
171+
172+
if (withBitmaps.length > 0) {
173+
console.log("");
174+
console.group(
175+
`%c⚠️ SVGs with Embedded Bitmaps (${withBitmaps.length})`,
176+
"color: #f59e0b; font-weight: bold;",
177+
);
178+
179+
withBitmaps.forEach((r) => {
180+
console.log("");
181+
console.log(`%cπŸ“„ ${r.name}`, "font-weight: bold;");
182+
console.log(` SVG size : ${formatSize(r.transferSize)}`);
183+
console.log(` Compression : ${r.compression}`);
184+
r.bitmaps.forEach((b) => {
185+
if (b.kind === "inline") {
186+
console.log(` πŸ–ΌοΈ inline ${b.format.toUpperCase()} β€” ~${formatSize(b.estimatedBytes)}`);
187+
} else {
188+
console.log(` πŸ”— external ${b.format.toUpperCase()} β€” ${b.url}`);
189+
}
190+
});
191+
});
192+
193+
console.groupEnd();
194+
195+
console.log("");
196+
console.group("%cπŸ’‘ Recommendations", "color: #3b82f6; font-weight: bold;");
197+
console.log("");
198+
console.log(" Embedded bitmaps inflate SVG size (~33% overhead for base64),");
199+
console.log(" block format negotiation, and prevent independent caching.");
200+
console.log("");
201+
console.log(" βœ… Extract the bitmap and serve it as a separate resource");
202+
console.log(" βœ… Convert the bitmap to a modern format (AVIF or WebP)");
203+
console.log(' βœ… Reference it from the SVG: <image href="image.avif" width="…" height="…"/>');
204+
console.log(" βœ… Both files are then cached and optimised independently");
205+
console.groupEnd();
206+
} else {
207+
console.log("");
208+
console.log(
209+
"%cβœ… No embedded bitmaps found in SVG resources.",
210+
"color: #22c55e; font-weight: bold;",
211+
);
212+
}
213+
214+
console.groupEnd();
215+
})();
216+
```
217+
218+
### Understanding the Results
219+
220+
#### Summary
221+
222+
| Field | Description |
223+
| ----------------------- | --------------------------------------------------------------------------------------------------------- |
224+
| External SVG files | Count of SVG resources captured by the Performance API (loaded via `<img>`, `<object>`, CSS, fetch, etc.) |
225+
| Inline `<svg>` elements | Count of `<svg>` elements present in the DOM |
226+
| SVGs with bitmaps | SVGs (external or inline) that contain at least one embedded bitmap |
227+
228+
#### External SVG Resources Table
229+
230+
| Column | Description |
231+
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
232+
| `name` | Filename extracted from the resource URL |
233+
| `size` | Bytes transferred over the network (compressed). `β€”` means the entry was served from cache with no transfer |
234+
| `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 |
235+
| `embedded bitmap` | βœ… none β€” no bitmaps detected Β· ⚠️ N found β€” N embedded bitmaps |
236+
237+
#### Compression Values
238+
239+
| Value | Meaning |
240+
| ------------ | ---------------------------------------------------------- |
241+
| `gzip` | Compressed with GZIP |
242+
| `br` | Compressed with Brotli |
243+
| `zstd` | Compressed with Zstandard |
244+
| `none` | No content encoding applied |
245+
| `compressed` | Compressed (type not accessible β€” CORS restriction) |
246+
| `cached` | Resource was served from cache with no transfer size |
247+
| `N/A` | Not applicable (inline `<svg>` element β€” no HTTP transfer) |
248+
249+
#### Embedded Bitmap Types
250+
251+
| Icon | Kind | Description |
252+
| ---- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
253+
| πŸ–ΌοΈ | `inline` | Base64-encoded bitmap embedded directly as a `data:image/…;base64,` URI. The reported size is an estimate derived from the base64 string length |
254+
| πŸ”— | `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 |
255+
256+
> **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.
257+
258+
### Further Reading
259+
260+
- [SVG `<image>` element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image) | MDN
261+
- [Data URLs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs) | MDN
262+
- [SVGOMG](https://jakearchibald.github.io/svgomg/) | jakearchibald.github.io
263+
- [SVGO](https://github.com/svg/svgo) | GitHub
264+
- [Image Element Audit](/Media/Image-Element-Audit) | webperf-snippets

β€Žpages/Media/_meta.jsonβ€Ž

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@
44
},
55
"Video-Element-Audit": {
66
"title": "Video Element Audit"
7+
},
8+
"SVG-Embedded-Bitmap-Analysis": {
9+
"title": "SVG Embedded Bitmap Analysis"
710
}
811
}

0 commit comments

Comments
Β (0)