Optimizing SVG Patterns to Their Smallest Size
Publikováno: 18.3.2022
I recently created a brick wall pattern as part of my #PetitePatterns series, a challenge where I create organic-looking patterns or textures in SVG within 560 bytes (or approximately the size of two tweets). To fit this constraint, I have …
Optimizing SVG Patterns to Their Smallest Size originally published on CSS-Tricks. You should get the newsletter.
I recently created a brick wall pattern as part of my #PetitePatterns series, a challenge where I create organic-looking patterns or textures in SVG within 560 bytes (or approximately the size of two tweets). To fit this constraint, I have gone through a journey that has taught me some radical ways of optimizing SVG patterns so that they contain as little code as possible without affecting the overall image quality.
I want to walk you through the process and show you how we can take an SVG pattern that starts at 197 bytes all the way down to a mere 44 bytes — a whopping 77.7% reduction!
The SVG pattern
This is what’s called a “running bond” brick pattern. It’s the most common brick pattern out there, and one you’ve surely seen before: each row of bricks is offset by one half the length of a brick, creating a repeating staggered pattern. The arrangement is pretty simple, making SVG’s <pattern>
element a perfect fit to reproduce it in code.
The SVG <pattern>
element uses a pre-defined graphic object which can be replicated (or “tiled”) at fixed intervals along the horizontal and vertical axes. Essentially, we define a rectangular tile pattern and it gets repeated to paint the fill area.
First, let’s set the dimensions of a brick and the gap between each brick. For the sake of simplicity, let’s use clean, round numbers: a width of 100
and a height of 30
for the brick, and 10
for the horizontal and vertical gaps between them.
Next, we have to identify our “base” tile. And by “tile” I’m talking about pattern tiles rather than physical tiles, not to be confused with the bricks. Let’s use the highlighted part of the image above as our pattern tile: two whole bricks in the first row, and one whole sandwiched between two half bricks in the second row. Notice how and where the gaps are included, because those need to be included in the repeated pattern tile.
When using <pattern>
, we have to define the pattern’s width
and height
, which correspond to the width and height of the base tile. To get the dimensions, we need a little math:
Tile Width = 2(Brick Width) + 2(Gap) = 2(100) + 2(10) = 220
Tile Height = 2(Bright Height) + 2(Gap) = 2(30) + 2(10) = 80
Alright, so our pattern tile is 220✕80
. We also have to set the patternUnits
attribute, where the value userSpaceOnUse
essentially means pixels. Finally, adding an id
to the pattern is necessary so that it can be referenced when we are painting another element with it.
<pattern id="p" width="220" height="80" patternUnits="userSpaceOnUse">
<!-- pattern content here -->
</pattern>
Now that we have established the tile dimensions, the challenge is to create the code for the tile in a way that renders the graphic with the smallest number of bytes possible. This is what we hope to end up with at the very end:
Initial markup (197 bytes)
The simplest and most declarative approach to recreate this pattern that comes to my mind is to draw five rectangles. By default, the fill
of an SVG element is black and the stroke
is transparent. This works well for optimizing SVG patterns, as we don’t have to explicitly declare those in the code.
Each line in the code below defines a rectangle. The width
and height
are always set, and the x
and y
positions are only set if a rectangle is offset from the 0
position.
<rect width="100" height="30"/>
<rect x="110" width="100" height="30"/>
<rect y="40" width="45" height="30"/>
<rect x="55" y="40" width="100" height="30"/>
<rect x="165" y="40" width="55" height="30"/>
The top row of the tile contained two full-width bricks, the second brick is positioned to x="110"
allowing 10
pixels of gap before the brick. Similarly there’s 10
pixels of gap after, because the brick ends at 210
pixels (110 + 100 = 210
) on the horizontal axis even though the <pattern>
width is 220
pixels. We need that little bit of extra space; otherwise the second brick would merge with the first brick in the adjacent tile.
The bricks in the second (bottom) row are offset so the row contains two half bricks and one whole brick. In this case, we want the half-width bricks to merge so there’s no gap at the start or the end, allowing them to seamlessly flow with the bricks in adjoining pattern tiles. When offsetting these bricks, we also have to include half gaps, thus the x
values are 55
and 165
, respectively.
Element reuse, (-43B, 154B total)
It seems inefficient to define each brick so explicitly. Isn’t there some way to optimize SVG patterns by reusing the shapes instead?
I don’t think it’s widely known that SVG has a <use>
element. You can reference another element with it and render that referenced element wherever <use>
is used. This saves quite a few bytes because we can omit specifying the widths and heights of each brick, except for the first one.
That said, <use>
does come with a little price. That is, we have to add an id
for the element we want to reuse.
<rect id="b" width="100" height="30"/>
<use href="#b" x="110"/>
<use href="#b" x="-55" y="40"/>
<use href="#b" x="55" y="40"/>
<use href="#b" x="165" y="40"/>
The shortest id
possible is one character, so I chose “b” for brick. The <use>
element can be positioned similarly to <rect>
, with the x
and y
attributes as offsets. Since each brick is full-width now that we’ve switched to <use>
(remember, we explicitly halved the bricks in the second row of the pattern tile), we have to use a negative x
value in the second row, then make sure the last brick overflows from the tile for that seamless connection between bricks. These are okay, though, because anything that falls outside of the pattern tile is automatically cut off.
Can you spot some repeating strings that can be written more efficiently? Let’s work on those next.
Rewriting to path (-54B, 100B total)
<path>
is probably the most powerful element in SVG. You can draw just about any shape with “commands” in its d
attribute. There are 20 commands available, but we only need the simplest ones for rectangles.
Here’s where I landed with that:
<path d="M0 0h100v30h-100z
M110 0h100v30h-100
M0 40h45v30h-45z
M55 40h100v30h-100z
M165 40h55v30h-55z"/>
I know, super weird numbers and letters! They all have meaning, of course. Here’s what’s happening in this specific case:
M{x} {y}
: Moves to a point based on coordinates.z
: Closes the current segment.h{x}
: Draws a horizontal line from the current point, with the length ofx
in the direction defined by the sign ofx
. Lowercasex
indicates a relative coordinate.v{y}
: Draws a vertical line from the current point, with the length ofy
in the direction defined by the sign ofy
. Lowercasey
indicates a relative coordinate.
This markup is much more terse than the previous one (line breaks and indentation whitespace is only for readability). And, hey, we’ve managed to cut out half of the initial size, arriving at 100 bytes. Still, something makes me feel like this could be smaller…
Tile revision (-38B, 62B total)
Doesn’t our pattern tile have repeating parts? It’s clear that in the first row a whole brick is repeated, but what about the second row? It’s a bit harder to see, but if we cut the middle brick in half it becomes obvious.
Well, the middle brick isn’t exactly cut in half. There’s a slight offset because we also have to account for the gap. Anyways, we just found a simpler base tile pattern, which means fewer bytes! This also means we have to halve the width
of our <pattern>
element from 220 to 110.
<pattern id="p" width="110" height="80" patternUnits="userSpaceOnUse">
<!-- pattern content here -->
</pattern>
Now let’s see how the simplified tile is drawn with <path>
:
<path d="M0 0h100v30h-100z
M0 40h45v30h-45z
M55 40h55v30h-55z"/>
The size is reduced to 62 bytes, which is already less than a third of the original size! But why stop here when there’s even more we can do!
Shortening path commands (-9B, 53B total)
It’s worth getting a little deeper into the <path>
element because it provides more hints for optimizing SVG patterns. One misconception I’ve had when working with <path>
is regarding how the fill
attribute works. Having played a lot with MS Paint in my childhood, I’ve learned that any shape I want to fill with a solid color has to be closed, i.e. have no open points. Otherwise, the paint will leak out of the shape and spill over everything.
In SVG, however, this is not true. Let me quote the spec itself:
The fill operation fills open subpaths by performing the fill operation as if an additional “closepath” command were added to the path to connect the last point of the subpath with the first point of the subpath.
This means we can omit the close path commands (z
), because the subpaths are considered automatically closed when filled.
Another useful thing to know about path commands is that they come in uppercase and lowercase variations. Lowercase letters mean that relative coordinates are used; uppercase letters mean absolute coordinates are used instead.
It’s a little trickier than that with the H
and V
commands because they only include one coordinate. Here’s how I would describe these two commands:
H{x}
: Draws a horizontal line from the current point to coordinatex
.V{y}
: Draws a vertical line from the current point to coordinatey
.
When we are drawing the first brick in the pattern tile, we start from the (0,0)
coordinates. We then draw a horizontal line to (100,0)
and a vertical line to (100,30)
, and finally, draw a horizontal line to (0,30)
. We used the h-100
command in the last line, but it is the equivalent of H0
, which is two bytes instead of five. We can replace two similar occurrences and pare the code of our <path>
down to this:
<path d="M0 0h100v30H0
M0 40h45v30H0
M55 40h55v30H55"/>
Another 9 bytes shaved off — how much smaller can we go?
Bridging (-5B, 48B total)
The longest commands standing in our way of a fully-optimized SVG pattern are the “move to” commands which take up 4, 5, and 6 bytes, respectively. One constraint we have is that:
A path data segment (if there is one) must begin with a “moveto” command.
But that’s okay. The first one is the shortest anyways. If we swap the rows, we can come up with a path definition where we only have to move either horizontally or vertically between the bricks. What if we could use the h
and v
commands there instead of M
?
The above diagram shows how the three shapes can be drawn with a single path. Note that we are leveraging the fact that the fill
operation automatically closes the open part between (110,0)
and (0,0)
. With this rearrangement, we also moved the gap to the left of the full-width brick in the second row. Here’s how the code looks, still broken into one brick per line:
<path d="M0 0v30h50V0
h10v30h50
v10H10v30h100V0"/>
Surely, we’ve found the absolute smallest solution now that we’re down to 48 bytes, right?! Well…
Digit trimming (-4B, 44B total)
If you can be a bit flexible with the dimensions, there’s another little way we can optimize SVG patterns. We’ve been working with a brick width of 100
pixels, but that’s three bytes. Changing it to 90
means one less byte whenever we need to write it. Similarly, we used a gap of 10
pixels — but if we change it to 8
instead, we save a byte on each of those occurrences.
<path d="M0 0v30h45V0
h8v30h45
v8H8v30h90V0"/>
Of course, this also means we have to adjust the pattern dimensions accordingly. Here’s the final optimized SVG pattern code:
<pattern id="p" width="98" height="76" patternUnits="userSpaceOnUse">
<path d="M0 0v30h45V0h8v30h45v8H8v30h90V0"/>
</pattern>
The second line in the above snippet — not counting the indentations — is 44 bytes. We got here from 197 bytes in six iterations. That’s a chunky 77.7% size reduction!
I’m wondering though… is this really the smallest size possible? Have we looked at all possible ways to optimize SVG patterns?
I invite you to try and further minify this code, or even experiment with alternative methods for optimizing SVG patterns. I would love to see if we could find the true global minimum with the wisdom of the crowd!
More on creating and optimizing SVG patterns
If you are interested to learn more about creating and optimizing SVG patterns, read my article about creating patterns with SVG filters. Or, if you want to check out a gallery of 60+ patterns, you can view the PetitePatterns CodePen Collection. Lastly, you’re welcome to watch my tutorials on YouTube to help you get even deeper into SVG patterns.
Optimizing SVG Patterns to Their Smallest Size originally published on CSS-Tricks. You should get the newsletter.