|
| 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 |
0 commit comments