Skip to content
SVGtoReact

Optimize SVG Without Breaking the Artwork

Optimize SVG files safely with SVGO. See which plugins remove viewBox, rename IDs, drop strokes, or merge paths, and how to configure and check the result.

By SVG to React. Published . Updated .

SVG files exported from design tools are bigger than they need to be. They carry editor metadata, comments, default values, redundant groups, and coordinates with more decimal places than any screen can show. Removing all of that is safe almost all the time. The rest of the time it breaks something, and the break rarely shows in a quick preview of the file itself. This post explains how to optimize SVG with SVGO, which plugins cause the surprises, and how to configure and check the result.

What an optimizer does

SVGO is the standard tool. It parses the SVG into a tree, runs a list of plugins over it, and prints the result. Its preset-default bundles the plugins its maintainers consider safe for a standalone file. Grouped by effect, they do the following:

The first two groups almost never cause trouble. The last two change the structure of the document. They are the source of most breakage, because other code may depend on that structure.

A real example of what changes

This file has a gradient, an ID on the shape, and a stroke width that has no visible effect yet, because no stroke color is set:

card.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  <defs>
    <linearGradient id="brand-fill">
      <stop offset="0" stop-color="#0f766e"/>
    </linearGradient>
  </defs>
  <rect id="outline" x="2" y="2" width="20" height="20"
    fill="url(#brand-fill)" stroke-width="2"/>
</svg>

This is the output of SVGO 4 with its default configuration, unedited:

card.min.svg (SVGO 4 preset-default)
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><linearGradient id="a"><stop offset="0" stop-color="#0f766e"/></linearGradient></defs><path fill="url(#a)" d="M2 2h20v20H2z"/></svg>

Viewed alone, the two files render identically. That is exactly what SVGO promises. But four things changed that code outside the file might depend on:

Now imagine the page that uses this SVG inline has this stylesheet:

styles.css
/* This stylesheet expects the original file */
#outline { stroke: #0f172a; }               /* id is gone */
.card rect { stroke: currentColor; }         /* rect is now a path */
                                             /* and stroke-width="2" was removed */

Every rule silently stops working. Nothing in SVGO is buggy here. It optimized the file it was given, and it cannot see your stylesheet. That is the central point of this post.

The plugins to think about

cleanupIds

It removes IDs nothing in the file references and shortens the rest to a, b, and so on. Short IDs are a problem when several SVGs are inlined into one page, because IDs are global to the document. Two optimized icons that both define a gradient a will share one. Either disable the plugin or use prefixIds, which is not part of the preset, to give each file its own prefix. It is also unsafe when CSS, JavaScript, or a <use href="icons.svg#id"> in another file refers to an ID.

removeUselessStrokeAndFill

It removes stroke attributes when no stroke is painted, and fill attributes when fill is none. That is correct for the file, and wrong if your CSS adds a stroke later. The plugin skips files that contain a <style> element or scripts, but it cannot see an external stylesheet.

convertShapeToPath and mergePaths

They make files smaller by turning rect, line, and polygon elements into paths and by joining adjacent paths that share attributes. Selectors that target an element type stop matching. So do animations that move one part of the drawing, and click handlers attached to one shape. If you plan to animate or script parts of an SVG, turn these off and give the parts IDs or classes you control.

convertPathData and cleanupNumericValues

They round coordinates, by default to three decimal places in path data. For a 24-unit icon that is far below a pixel. If an artwork uses a tiny viewBox, such as 0 0 1 1, three decimals can be visible, so raise floatPrecision for files like that.

removeViewBox and removeTitle

Removing viewBox stops an SVG from scaling, and removing <title> removes its accessible name. SVGO 4 took both plugins out of preset-default, so they no longer run unless you add them. SVGO 3 still includes them, and so do tools that bundle SVGO 3, such as SVGR 8's SVGO plugin. If you use one of those, override both. removeDesc is still in the preset, but by default it removes only an empty <desc> or one that an editor generated.

A configuration for SVGs used in code

For icons that are inlined into pages or turned into components, and then styled from outside, this configuration keeps the structure that outside code relies on. All the other savings stay:

svgo.config.mjs
// svgo.config.mjs
export default {
  multipass: true,
  plugins: [
    {
      name: "preset-default",
      params: {
        overrides: {
          // Keep IDs that CSS, JavaScript, or other files refer to
          cleanupIds: false,
          // Keep rect, circle, and line elements so selectors and animations still match
          convertShapeToPath: false,
          // Keep stroke attributes that a stylesheet will activate later
          removeUselessStrokeAndFill: false,
        },
      },
    },
    // Not in preset-default: give every ID a per-file prefix instead
    { name: "prefixIds", params: { prefix: "icon-check" } },
  ],
};

For SVGs used only as images, such as an img tag or a CSS background, the default preset is usually fine as it is. Nothing outside the file can see inside it anyway.

SVGO's CLI reads the config file automatically from the working directory, or you can pass it explicitly:

Terminal
npx svgo icon.svg -o icon.min.svg
npx svgo -f assets/icons -o assets/icons-min
npx svgo --config svgo.config.mjs -f assets/icons -o assets/icons-min

How to check the result

Terminal
# Compare what is actually sent, not just raw bytes
gzip -9 -c icon.svg | wc -c
gzip -9 -c icon.min.svg | wc -c

Optimize once, on the source file, and commit the result. Running a second optimizer at build time on a file that is already optimized rarely saves more, and it adds another place where defaults can differ.

Optimizing in the browser

The SVG optimizer on this site runs SVGO 4's preset-default with multipass in your browser and shows the size before and after. viewBox, width, height, and title stay. The exact list is in What the Optimize Pass Keeps in SVG. It uses the default preset, so it shortens IDs and converts shapes as shown above. Use it for files that will be used as images or converted into components. For files that a stylesheet targets, run SVGO locally with a config like the one in this post.

The SVG to React converter has the same optimizer as an Optimize switch, which is off by default. Turning a file into a component changes what is safe again. The component scopes nothing, so the ID advice applies doubly. See SVG to React Component: A Practical Guide for generating unique IDs with useId.

Questions

Is SVGO preset-default safe to run on any file?

It is designed to keep rendering the same for a standalone file, but it can still break things outside the file: IDs that CSS or JavaScript reference, strokes added later by a stylesheet, and paths you planned to animate separately. Check those cases before you ship the output.

Does SVGO remove the viewBox?

SVGO 4 does not, because removeViewBox is no longer part of preset-default. SVGO 3 includes it in the default preset, and older tools that bundle SVGO 3, such as SVGR 8's SVGO plugin, can still drop viewBox unless you override that plugin.

What is multipass?

With multipass on, SVGO repeats the plugin pipeline until the output stops getting smaller, up to a fixed number of passes. One plugin's change can create a new opportunity for another, such as collapsing a group after its attributes have moved.

Should I gzip or optimize?

Both. They solve different problems. SVGO removes data the renderer does not need, and gzip or Brotli compresses whatever is left in transit. Compare optimized and original files after compression when you report savings, because raw byte counts overstate the difference.