How SVG Rendering Works: Path Data, the DOM, and Where Optimizers Find Savings
The d attribute's command grammar, why exported SVGs bloat, and how the browser turns path data into pixels at every zoom level.
Updated July 2026 · 10 min read
The practical guide to SVG optimization covers what to strip and when to inline versus link a file. This one goes underneath that: what the d attribute actually says, why an exporter and a human produce wildly different byte counts for the same shape, and what the browser does with that string between parsing it and putting pixels on screen.
The path data mini-language
Everything a <path> draws lives in one attribute, d, whose value is a tiny language of its own: a sequence of single-letter commands, each followed by the numeric arguments that command needs. There's no punctuation beyond whitespace and optional commas between numbers — a parser just reads a letter, then consumes however many numbers that letter requires, then looks for the next letter.
The commands that matter in practice:
- M / m — moveto. Picks up the pen and puts it down at a point without drawing. Starts every subpath.
- L / l — lineto. Draws a straight segment from the current point to the given point.
- C / c — cubic Bézier curveto. Takes three coordinate pairs: the first control point, the second control point, and the endpoint. The curve leaves the current point heading toward the first control point and arrives at the endpoint heading away from the second.
- Q / q — quadratic Bézier curveto. One coordinate pair for a single control point, then one for the endpoint — cheaper to encode than a cubic, less flexible in shape.
- A / a — elliptical arc. Six arguments: rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, then the endpoint x,y. The two flags disambiguate which of the (up to four) possible arcs between the current point and the endpoint gets drawn, since an ellipse with given radii can connect two points multiple ways.
- Z / z — closepath. Draws a straight line back to the start of the current subpath and, unlike the others, takes no arguments.
Zandzare identical.
Every letter other than Z comes in two cases, and the case is the single most consequential fact about path data: uppercase means the coordinates that follow are absolute, anchored to the SVG's coordinate system, while lowercase means the coordinates are relative to wherever the pen currently sits. L 90 10 means "draw a line to the point (90, 10)." l 80 0 means "draw a line 80 units right and 0 units down from here" — a completely different instruction that happens to draw the same segment if the pen was already at (10, 10). Mix the two freely in one path; the parser just tracks the current point and applies each command's rule as it goes.
Walking a path by hand
Take a minimal triangle:
M10 10 L90 10 L90 90 ZA renderer processes this left to right, one command at a time:
M10 10— lift the pen, place it at (10, 10). This becomes the current point and the start-of-subpath marker (needed later forZ).L90 10— draw a straight line from (10, 10) to (90, 10). Current point is now (90, 10). Because y didn't change, this reads as a horizontal line across the top of the shape.L90 90— draw a straight line from (90, 10) to (90, 90). Current point is now (90, 90). x didn't change here, so this is a vertical line down the right side.Z— draw a straight line from the current point (90, 90) back to the subpath's start, (10, 10). This is the diagonal hypotenuse, and it's implicit — nothing in the string says "(10, 10)" a second time.
Three explicit points and one closing rule produced a filled triangle. Written with relative commands instead — m10 10 l80 0 l0 80 z — it draws the exact same shape, because each lowercase command is just "from here, go this far."
Why exported SVGs balloon
Open a path exported from Illustrator, Figma, or Sketch and it rarely resembles the triangle above, even for equally simple geometry. Four habits account for almost all of the difference:
- Absolute everywhere. Exporters emit uppercase commands for every segment instead of switching to relative ones when the delta is small. A shape that could say
l5 0instead repeats a full pair of large absolute coordinates. - Excess decimal precision. Internal editor math routinely produces values like
10.0000001or47.38299999where10or47.383would render identically on screen. Every one of those extra digits is pure byte cost. - No deduplication. If the same curve or icon shape appears four times, a design tool typically writes out four full copies of the path data rather than defining it once and referencing it with
<use>. - Editor-only baggage. Namespaced attributes like
sodipodi:*orinkscape:*, empty wrapper<g>groups left over from layer panels, and<defs>blocks defining gradients or clip paths nothing in the visible document uses. None of it affects a single pixel; all of it round-trips back into the editor's own file format, which is what the export is actually optimized for.
The same rectangle outline, exported versus hand-optimized:
<!-- exported -->
<path d="M10.0000001,10.0000000 L90.0000004,10.0000000
L90.0000004,90.0000004 L10.0000001,90.0000004 Z"/>
<!-- hand-optimized -->
<path d="M10 10 h80 v80 h-80 Z"/>The optimized version does two things at once: it truncates precision (no shape needs eight decimal digits to hit an integer grid), and it swaps in h/v — horizontal and vertical lineto shorthands, each taking a single number instead of a full coordinate pair, since only one axis changes. Same rectangle, roughly a third of the characters.
What an optimizer's passes actually do
A tool like SVGO isn't one transformation, it's a pipeline of small, mostly independent passes, each targeting one of the patterns above:
| Pass | What it does |
|---|---|
| Strip metadata | Removes comments, XML declarations, editor namespaces, and unreferenced <defs> entries. |
| Collapse empty groups | Deletes <g> wrappers with no attributes and no useful nesting purpose. |
| Round coordinates | Truncates decimal precision to a configurable number of digits. This is the one pass with a real fidelity tradeoff — too aggressive and detailed curves visibly shift at high zoom. |
| Convert to relative | Rewrites absolute commands as relative ones (and picks whichever encoding is shorter per segment), plus swaps in shorthands like h/v/s/t where the geometry allows it. |
| Merge path segments | Combines consecutive commands of the same type (L x1 y1 L x2 y2 patterns and similar) and drops no-op segments that don't move the pen. |
| Collapse transforms | Multiplies chained transform matrices on nested elements down into a single matrix, or bakes the transform directly into the coordinates and removes the attribute. |
None of these passes change what the path describes geometrically (aside from precision rounding, which changes it by a deliberately negligible amount). They change how the same geometry is encoded — which is exactly why running the same file through an optimizer twice yields no further savings on the second pass.
From markup to pixels
What makes SVG behave so differently from a raster image starts before any of this — with what an <svg> element actually is. When the browser parses an inline <svg>, it isn't decoding image bytes; it's building ordinary DOM nodes, the same kind of tree it builds for <div> or <table>. Each <path>, <circle>, and <g> is a real node you can query with querySelector, mutate with JS, or target with a CSS selector, and it participates in the accessibility tree like any other element.
From that DOM, the rendering engine builds a render tree and, for each SVG shape, computes the actual geometry — resolving the path data, any transforms, and inherited styles — and scan-converts it: walking the shape's outline and deciding, at the current output resolution, exactly which pixels fall inside it and how to blend edges that only partially cover a pixel (anti-aliasing). Critically, that resolution isn't fixed. Zoom the page, resize the viewport, or scale the element with CSS, and the engine reruns scan conversion against the new pixel grid, using the same underlying path data. There's no stored bitmap to stretch — the shape is redrawn from its mathematical description every time the output size changes, which is the entire reason it stays crisp at any zoom level. A PNG has no equivalent step: its pixels were fixed at export time, so zooming can only interpolate between pixels that already exist, not recompute new ones.
Inline versus linked, and why it isn't just a style preference
Because an inlined SVG is genuinely part of the page's DOM, it can be styled and animated the way any other element can: a CSS rule can change one <path>'s fill on hover, a class toggle can trigger a stroke animation, JavaScript can read and rewrite the d attribute directly. None of that is available to a linked SVG — one loaded via <img src="icon.svg"> or a CSS background-image. The browser treats a linked SVG like any other image resource: it renders as an opaque box from the host page's point of view, so you can resize it, apply CSS filters to the whole image, or swap the file, but you cannot reach into it to recolor a single stroke from the parent document's stylesheet.
The tradeoff runs the other way for caching. A linked file is a normal HTTP resource — fetched once, cached by the browser, and reused across every page and every repeated <img> tag that points at it. An inlined <svg> has no independent existence as a resource; it's markup baked into the HTML (or hydrated into the DOM by JS), so it travels with every page that embeds it and gets no caching benefit of its own. A repeated icon used sitewide is usually cheaper linked; an icon that needs per-instance styling or a hover animation usually needs to be inline.
Try it
Paste a path from an actual export into the DevBox SVG Optimizer and diff the d attribute before and after — the absolute-to-relative conversion and the precision truncation are usually visible in the first few characters.
Frequently asked questions
Why does a hand-written path look nothing like an exported one?+
Mostly the case of the command letters. Hand-written paths mix relative commands (lowercase l, c, q) that only encode a delta from the current point, which stays short. Exporters default to absolute commands (uppercase) for every segment, so each one repeats the full coordinate pair even when the shape only moved a few units.
Does converting absolute path commands to relative ones change how the path looks?+
No. M10 10 L90 10 and M10 10 l80 0 draw the identical line — relative coordinates are just an offset from wherever the pen currently is. An optimizer only rewrites the encoding, never the geometry, unless it's also rounding precision.
Is it safe to truncate path coordinate precision?+
Almost always, but not unconditionally. Rounding 12.4999999 to 12.5 or even 12 is invisible at normal screen sizes because a pixel can't show a ten-thousandth of a unit anyway. At high zoom, or on a path built from many tiny segments where rounding error compounds, aggressive truncation can visibly shift edges. That's why optimizers expose the precision as a setting rather than hardcoding it.
Why does an SVG stay sharp when I zoom in but a PNG icon gets blurry?+
A PNG is already a fixed grid of pixels; zooming just stretches those pixels. An SVG has no pixels stored at all — it's a set of mathematical path and shape descriptions. The browser rasterizes (scan-converts) it fresh at whatever resolution the current zoom level needs, so the edges are recomputed, not stretched.
If I inline an SVG in my HTML, do I lose caching?+
Yes, for that specific instance. An inlined <svg> is parsed as part of the page's own DOM and is not a separate cacheable resource, so it's re-downloaded (as part of the HTML) on every page that inlines it. A linked file (<img src> or a CSS background-image) is fetched once and cached by the browser across every page that references it, at the cost of not being stylable per sub-element from the parent page's CSS.
Can I style individual parts of an SVG with CSS either way?+
Only if it's inlined. Inline SVG nodes are real DOM elements, so a rule like svg path:hover { fill: red; } works the same as it would on any other element. A linked SVG file is opaque from the parent document's perspective — you can resize or filter the whole image, but you can't reach inside it with the host page's stylesheet.