svgo

    SVG optimizer in pure MoonBit: svgo-compatible plugin pipeline with path data optimization, colour and number cleanup, and render-diff verification

    svg
    svgo
    optimizer
    minifier
    wasm
    frontend
    Download zip
    Version
    0.2.1
    License
    MIT
    Last updated
    1 hour ago
    Downloads
    12

    #svgo.mbt

    An SVG optimizer written in MoonBit, shipped as WebAssembly. All 34 plugins in svgo's preset-default, in its order and with its semantics, and none of the Node.js dependency tree: a 200 KB wasm-gc module that runs in the browser, Node, Bun and Deno, a CLI that installs with npx, and a MoonBit library.

    #Quickstart

    JavaScript (Node 24+, Bun 1.4+, Deno 2, Chrome 130+, Firefox 134+, Safari 18.4+; see Runtime compatibility):

    npm i @rivus/svgo

    import { optimize } from "@rivus/svgo"; const r = await optimize(svg, { precision: 3 }); r.data; r.originalSize; r.size; r.passes; r.applied;

    Command line, without a toolchain (Node 24+; bunx and deno run -A npm:@rivus/svgo work too):

    npx @rivus/svgo input.svg -o out.svg cat input.svg | npx @rivus/svgo > out.svg # stdin, or pass "-" as the input npx @rivus/svgo icons -r -o dist # a directory, recursively # installed (npm i -g @rivus/svgo) the command is `svgo-mbt`

    MoonBit:

    moon add PerfectPan/svgo

    ///|
    test "optimize" {
    let result = @svgo.optimize(
    (
    #|<svg xmlns="http://www.w3.org/2000/svg" width="10px" height="10px">
    #| <!-- Generator: Sketch -->
    #| <g fill="#FF0000" stroke="none">
    #| <rect x="0" y="0" width="10.000" height="10.000"/>
    #| </g>
    #|</svg>
    ),
    )
    inspect(
    result.data,
    content=(
    #|<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><path fill="red" d="M0 0h10v10H0z"/></svg>
    ),
    )
    }

    Config provides plugin selection, params : Map[String, Json], precision, multipass, and pretty printing; see the API reference for all fields.

    #Command line

    svgo-mbt input.svg # optimized SVG on stdout svgo-mbt input.svg -o out.svg --stats # write a file, print size statistics svgo-mbt input.svg -p 2 --pretty # 2 decimal places, indented output svgo-mbt input.svg --json # {data, originalSize, size, passes, applied} svgo-mbt input.svg --plugins convertPathData,sortAttrs svgo-mbt input.svg --param cleanupIds.preserve=logo,icon --param cleanupIds.minify=false svgo-mbt input.svg --disable convertShapeToPath --enable moveGroupAttrsToElems svgo-mbt --list # available plugins

    Exit codes: 0 on success, 1 for a usage error, 2 when a file could not be parsed (the names go to stderr, so a shell loop can act on them).

    --param <plugin>.<key>=<value> is repeatable. Values true/false become booleans, integers become numbers, comma-separated values become string arrays, and other values remain strings.

    #Plugins

    Enabled by default, in svgo's preset-default order:

    pluginwhat it does
    removeDoctype, removeXMLProcInst, removeComments, removeMetadatastrip the prolog and editor comments (<!--! ... --> legal comments are kept)
    removeEditorsNSDatadrop Inkscape / Sketch / Illustrator / Figma namespaces, elements and attributes
    cleanupAttrs, removeDesc, cleanupIds, removeUselessDefsnormalise whitespace, drop Created with ... descriptions, unused ids and unreferenced <defs> children
    cleanupNumericValuesround numbers, remove px
    convertColorsrgb() / names / #RRGGBB to the shortest form
    mergeStyles, inlineStyles, minifyStylesmerge <style> elements, inline matching rules to style="...", minify CSS and drop unused selectors
    removeUnknownsAndDefaultsdrop attributes equal to their defaults, unless an ancestor overrides them
    removeDeprecatedAttrsdrop attributes the SVG spec deprecated, keeping the ones that still render
    removeNonInheritableGroupAttrsdrop presentation attributes off a <g> that children cannot inherit
    removeUselessStrokeAndFilldrop stroke-* when nothing is stroked, fill-* when nothing is filled
    cleanupEnableBackgrounddrop enable-background unless a filter uses BackgroundImage
    removeHiddenElems, removeEmptyTextzero-size shapes, display:none, empty paths and texts
    convertShapeToPath, convertEllipseToCirclerect / line / polyline / polygon to path, equal-radius ellipses to circles
    moveElemsAttrsToGrouplift an attribute every child shares onto their group
    collapseGroupsunwrap groups and push their attributes down
    convertPathDataelement transforms baked into the data, curve runs to arcs, relative/absolute per segment, H/V/S/T shorthands, straight curves to lines, precision with drift compensation
    convertTransformmultiply, decompose and round transform lists, drop identity and redundant ones
    mergePathsjoin adjacent paths with identical attributes when their outlines do not intersect
    removeEmptyAttrs, removeEmptyContainers, removeUnusedNS, sortAttrs, sortDefsChildrenfinal cleanup

    Optional: removeDimensions, removeTitle, moveGroupAttrsToElems.

    Parameters use svgo's names and JSON shapes. Each plugin receives only its own object; omitted fields use these defaults:

    PluginParameters and defaults
    cleanupIdsremove: true, minify: true, preserve: [], preservePrefixes: [], force: false
    convertColorscurrentColor: false, names2hex: true, rgb2hex: true, shorthex: true, shortname: true
    convertShapeToPathconvertArcs: false, floatPrecision: config precision
    convertTransformfloatPrecision: config precision (3), transformPrecision: 5 (limited to matrix precision), degPrecision: derived from matrix digits and float precision
    removeCommentspreservePatterns: ["^!"]; false or [] removes all comments
    removeUnknownsAndDefaultsunknownContent, unknownAttrs, defaultAttrs, uselessOverrides, keepDataAttrs, keepAriaAttrs: all true; keepRoleAttr: false
    removeUselessStrokeAndFillstroke: true, fill: true, removeNone: false
    sortAttrsxmlnsOrder: "front"; order: ["id", "width", "height", "x", "x1", "x2", "y", "y1", "y2", "cx", "cy", "r", "fill", "stroke", "marker", "d", "points"]

    preserve and preservePrefixes accept one string or an array of strings. currentColor accepts a boolean or an exact color string and does not replace colors inside masks. preservePatterns supports literal substring matches and ^-prefixed literal prefix matches, not full regular expressions.

    #Performance

    Same Node process, both with multipass, milliseconds per call (node packages/compare/collect.mjs, Node 24, svgo 4.1.0):

    filesizesvgo.mbt (wasm-gc)svgo-jsratio
    sketch-icon.svg836 B0.1190.3192.7×
    inkscape-drawing.svg2 KB0.1880.6693.6×
    SVG_logo.svg4 KB0.5791.6742.9×
    Tux.svg50 KB7.09214.2382.0×
    Ghostscript_Tiger.svg68 KB12.48536.0162.9×
    World map (low resolution)85 KB11.88951.8674.4×

    Between 1.7× and 4.4× depending on the file, 2.9× at the median. Cold start on a small icon, whole process (scripts/cli-bench.sh): native CLI about 10 ms, Node build about 50 ms, svgo CLI about 160 ms.

    #Runtime compatibility

    ArtifactRuns onChecked by
    @rivus/svgo (wasm-gc)Node 24+, Bun 1.4+, Deno 2 (tested with 2.9); Chrome 130+, Firefox 134+, Safari 18.4+CI runs the 330-case fixture suite through the shipped wasm under Node, Bun and Deno on every push
    svgo-mbt CLI, Node buildthe same runtimes, via npx / bunx / deno run -Asame suite plus a CLI smoke test
    svgo-mbt native binarylinux x86_64, linux arm64, macOS arm64 from GitHub releases; any other platform with moon build --target native --release app/clibuilt by binaries.yml on each release
    PerfectPan/svgo MoonBit librarytargets wasm-gc, js and native; current MoonBit toolchain (CI installs the latest release)moon test on js and native in CI

    The wasm needs an engine with WebAssembly GC and JS String Builtins (imported string constants). Older engines fail at instantiate rather than at runtime: Node 22 and 23 report Import module="_", Bun 1.2 reports import wasm:js-string:length must be an object. This cannot be polyfilled from JavaScript because fromCharCodeArray takes a wasm-gc array. The optimizer itself does no I/O; the loader fetches the wasm over http(s) and reads file: URLs through node:fs, which Bun and Deno provide.

    #Compatibility with svgo

    Upstream cases (svgo e4cb29b, 2026-08-27):

    cases
    pass289
    known differences0
    skipped0

    moveGroupAttrsToElems ships opt-in (--enable moveGroupAttrsToElems). Pushing a group's transform onto its children lets convertPathData bake the matrix into every path, which lengthens coordinates and leaves formerly identical siblings unmergeable. Across the test corpus files it costs 15,859 bytes on the Ghostscript tiger and saves 48 bytes across the rest. svgo shows the same effect on that file (68,101 bytes with the plugin, 52,029 without, against 52,690 for svgo.mbt).

    svgo also has 19 opt-in plugins; removeDimensions and removeTitle are implemented, and the other 17 are not.

    #Verification

    scripts/verify.sh # check, fmt, .mbti drift, fixtures, tests on js / native scripts/verify.sh --full # + wasm build, sizes vs svgo-js, resvg pixel diff, speed (needs pnpm install)

    Runs unit tests, svgo's own fixture suite, resvg pixel diff, and byte-for-byte corpus checks. Details and developer guides: docs/ARCHITECTURE.md, CONTRIBUTING.md, AGENTS.md.

    #License

    MIT. Plugin semantics follow svgo (MIT, © Kir Belevich and contributors).

    UnknownPlugin

    pub suberror UnknownPlugin {
    UnknownPlugin(String)
    } derive(
    Debug
    )

    Raised by optimize when Config::plugins names a plugin that does not exist.

    Config

    pub(all) struct Config {
    plugins : Array[String]
    params : Map[String, Json]
    precision : Int
    multipass : Bool
    pretty : Bool
    } derive(
    Debug
    )

    What to run and how to print: the plugin list in execution order, numeric precision, multipass and pretty printing. Start from Config::default() and override fields with record update syntax.

    Config::default

    fn Config::default() -> Config

    svgo's preset-default plugins in svgo's order (except the opt-in moveGroupAttrsToElems), 3 decimal places, multipass on, compact output.

    Result

    pub(all) struct Result {
    data : String
    original_size : Int
    size : Int
    passes : Int
    applied : Array[String]
    } derive(
    Debug
    )

    Outcome of optimize: the optimized document plus size statistics and the list of plugins that changed something.

    Result::saved_percent

    fn Result::saved_percent(self : Result) -> Double

    Bytes saved as a percentage of the original size (0 for an empty input).

    list_plugins

    fn list_plugins() -> Array[(String, String, Bool)]

    Names of all available plugins with their descriptions.

    optimize

    fn optimize(svg : String, config? : Config) -> Result raise

    Optimize an SVG document.

    Source Files