My trick for compressing SVGs
3/25/2025
Written by James Merrill
I recently released a new artwork entitled BUSY. It has two main creative inputs. One is a very long and complex Javascript file that performs computations, often using randomness. The other is a series of illustrations that I've drawn. These two approaches are combined and work harmoniously, the code manipulating and transforming the vector data from my drawings.
The artwork ends up in two places. First, I utilized a pen plotter to draw it onto large pieces of paper, and second, I've archived it on the Ethereum blockchain.
Combining approaches
The visuals of BUSY are the sum of many thousands of calculations performed by various algorithms. Random walkers, Poisson disk sampling, Gaussian distributions, and occlusion are all used to create eye-pleasing geometric forms.
Algorithms have limits, though, and I devised a trick that introduced a human touch to the work. I used hand-drawn vector data for each artwork's hundreds of buildings, cars, and trees. These "sprites" were created in Adobe Illustrator and saved as SVG files.
I kept a close eye on how new illustrations were ballooning my payload size. After a few months of accumulating different drawings, they occupied around 1.5 MB of disk space. It was important to me that the entire set of instructions for the art made its way on to the blockchain, and file size became a cost issue.
For reference, my project ORI cost around $900 to archive in 2022, and it was only around 60kb. I was motivated to find a way to condense my illustrations into their smallest possible expression.
Distilling
I started by converting the SVG data into polygons and vertices and storing them as JSON. This approach eliminated the bulky XML markup inside of an SVG file.
However, the vertex arrays inside JSON were non-optimal for a few reasons:
- JSON is still bulky, with numerous curly braces, commas, and quotations.
- Going from
<circle cx="50" cy="50" r="50" />
to an array of 18+ vertices[[0,0], ....[0,0]]
was a net loss.
I also realized that my buildings contained a lot of repeated geometry, specifically the windows and doors—another opportunity to reduce file size with instancing.
To remove the heft of JSON's extra characters, I opted for a flat file, where each line was a piece of geometry.
#a-box
r0,0,10,10
#a-circle
c0,0,5
#a-line
0,0,10,10
I expressed geometry in the form of vertices with integers. Still, utilizing only polygons to make simple shapes requires many bytes.
In order to optimize my payload, I went further and made new primitives for each commonly used shape containing their relevant dimensions such as x,y coordinates, radius, etc.
This logic became a converter script that would accept a directory of SVGs and output a single flat file. It included an optimized transform for all elements included in SVGs. On the client, I used Vite to include the flat file, and an unpacking procedure to use these instructions to draw geometry.
I began to think about groups of repeated elements. A building with 100+ windows would create a long list of polygons to draw when, in reality, each window could be an instance of a single set of directions.
#building-1 //Not optimal
r0,0,10,10
c0,0,5
l2,2,4
r20,0,10,10
c20,0,5
l22,2,4
r40,0,10,10
c40,0,5
l42,2,4
&window //Let's create an reference for a window
r0,0,10,10
c0,0,5
l2,2,4
#building-1 //Now we can instance the window many times.
...
@window 0,0
@window 20,0
@window 40,0
This was my biggest opportunity to cut down on data. Since my buildings were mostly windows, and windows were mostly repeated, I knew a clever solution would result in the biggest optimization. I added some logic to detect these repeated groups of geometry and optimizing their storage into single references.
Paths were my next big opportunity. They often occupied many bytes of vertices and were also repeated across sprites. I took the same approach of tabulating all paths, comparing them, and instancing when appropriate.
The creators of the SVG spec found an eloquent solution to describing paths in a shorthand syntax.
<path d="M 10 10 H 90 V 90 H 10 L 10 10"/>
My initial approach involved extracting this syntax into my flat file and deferring their conversion to vertices until the client unpacked the payload. As I began this implementation, I encountered many different instructions in the path syntax that soon made my conversion code quite significant in its own right.
I weighed the tradeoffs and decided to convert my paths into polygons early in the build process, with some optimizations to reduce their vertex counts.
Final results
After implementing these techniques, I dropped my 1.4mb of SVG data to 108kb~ of instructions. BUSY was successfully archived on the blochchain, and is being exhibited on Artblocks.io.