A Client-Side SVG Optimizer That Never Sends Your Files Anywhere
SVGO is great, but it's a Node CLI. Online SVG optimizers are convenient but upload your files to someone's server. This one runs entirely in your browser: paste SVG, get optimized SVG, download or copy. 12 configurable transformations, 52 tests, zero network calls.
SVG files from design tools are bloated. Figma exports include layer IDs, Sketch exports have metadata comments, Illustrator adds XML declarations nobody needs. An SVG icon that should be 400 bytes ends up 4KB. The optimizations to fix this are well-known — the challenge is doing them in a browser without a server round-trip.
🔗 Live demo: https://sen.ltd/portfolio/svg-optimizer/
📦 GitHub: https://github.com/sen-ltd/svg-optimizer
Features:
- 12 configurable transformations
- Side-by-side preview (original vs optimized)
- Real-time size stats
- Download / copy / drop-zone upload
- Fully client-side (no upload)
- Japanese / English UI
- Zero dependencies, 52 tests
Transformations
Each transformation is a pure string → string function:
export function removeComments(svg) {
return svg.replace(/<!--[\s\S]*?-->/g, '');
}
export function removeXMLDeclaration(svg) {
return svg.replace(/<\?xml[^?]*\?>/g, '');
}
export function removeDoctype(svg) {
return svg.replace(/<!DOCTYPE[^>]*>/g, '');
}
export function collapseWhitespace(svg) {
return svg.replace(/\s+/g, ' ').replace(/>\s+</g, '><').trim();
}
Simple regex replacements. No DOMParser, no AST. For the transformations an SVG optimizer needs, string manipulation is both sufficient and fast.
Rounding numeric attributes
Design tools often emit coordinates like d="M 12.3456789,45.6789012 L ...". Most of those decimals are meaningless for rendering. Rounding to 2-3 decimals can cut path strings in half:
export function roundNumbers(svg, decimals = 2) {
const factor = Math.pow(10, decimals);
return svg.replace(/-?\d+\.\d+/g, (match) => {
const n = Math.round(parseFloat(match) * factor) / factor;
return n.toString();
});
}
The regex matches only decimals (not integers), so M 100,200 stays as M 100,200 while M 100.12345,200.67890 becomes M 100.12,200.68.
Removing default attributes
SVG has many attributes that default to specific values. opacity="1", fill-opacity="1", stroke-width="1", stop-opacity="1" — all unnecessary if they match the defaults:
const DEFAULTS = {
'opacity': '1',
'fill-opacity': '1',
'stroke-opacity': '1',
'stroke-width': '1',
'stop-opacity': '1',
};
export function removeDefaultAttrs(svg) {
let result = svg;
for (const [attr, defaultValue] of Object.entries(DEFAULTS)) {
const pattern = new RegExp(`\\s${attr}="${defaultValue}"`, 'g');
result = result.replace(pattern, '');
}
return result;
}
Editor artifacts
Design tools leave breadcrumbs. Sketch adds sketch:type, Figma uses specific id patterns, Inkscape litters inkscape:* attributes. None affect rendering:
export function removeEditorIds(svg) {
return svg
.replace(/\s(sketch|figma|inkscape|sodipodi):[^\s"=]+="[^"]*"/g, '')
.replace(/\sid="(layer|path|g|rect)\d+"/g, '');
}
Unused namespace cleanup
If xmlns:inkscape was declared but inkscape:* attributes were all removed, the namespace itself is now dead weight. Detect unused namespaces by searching for prefix: usage after stripping the declaration:
export function removeUnusedNamespaces(svg) {
let result = svg;
const nsPattern = /\sxmlns:(\w+)="[^"]*"/g;
const matches = [...svg.matchAll(nsPattern)];
for (const [fullMatch, prefix] of matches) {
const afterRemoval = result.replace(fullMatch, '');
if (!afterRemoval.includes(`${prefix}:`)) {
result = afterRemoval;
}
}
return result;
}
Never remove the default xmlns="http://www.w3.org/2000/svg" — that's required for the SVG to render at all.
Size stats
export function getSizeStats(original, optimized) {
const encoder = new TextEncoder();
const originalBytes = encoder.encode(original).length;
const optimizedBytes = encoder.encode(optimized).length;
return {
originalBytes,
optimizedBytes,
savedBytes: originalBytes - optimizedBytes,
savedPercent: ((1 - optimizedBytes / originalBytes) * 100).toFixed(1),
};
}
TextEncoder gives accurate UTF-8 byte counts — important because .length on a JS string counts UTF-16 code units, which differ from on-disk bytes for anything containing non-ASCII.
Series
This is entry #60 in my 100+ public portfolio series.
- 📦 Repo: https://github.com/sen-ltd/svg-optimizer
- 🌐 Live: https://sen.ltd/portfolio/svg-optimizer/
- 🏢 Company: https://sen.ltd/

Top comments (0)