DEV Community

SEN LLC
SEN LLC

Posted on

A Client-Side SVG Optimizer That Never Sends Your Files Anywhere

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

Screenshot

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
  });
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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, '');
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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),
  };
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)