My trick for compressing SVGs

3/25/2025

Written by James Merrill

I've spent about a year creating a new generative artwork entitled BUSY. Although the work starts as Javascript, it is drawn by my Axidraw A1 pen plotter onto paper with 5-8 different inks, which takes up to 12 hours.

The intriguing contradiction of a generative program is that it is both infinite and ephemeral. Each time the program is executed, a pseudo-random number generator (in my case, P5.JS's random() ) creates a new series of values that directly influence the resulting visual output. When the program is re-run, that unique series of values is forever gone, along with the visual you saw.

I enjoy plotting these works to "freeze" them into being. Transferring them to paper gives them a certain level of durability, which, with care, will last for many decades. The drawings are always unique from one another, too, since they always contain a different PRNG seed.

BUSY (2025)

Bundle sizes

The visuals of BUSY are the sum of many thousands of calculations performed by various algorithms. Random walkers, Poisson disk sampling, Gaussian distributions, and geometric occlusion are a few of the techniques under the hood.

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.

A subset of the "sprites" that I've drawn for BUSY

A problem arose - they occupied around 1.25 MB of disk space in this format. Making uncompressed SVG data effectively a non-starter for storage on the blockchain.

Finding a solution

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.

Building illustrations converted to vertices

However, the vertex arrays inside JSON were non-optimal for a few reasons:

  1. JSON is still bulky, with numerous curly braces, commas, and quotations.
  2. 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.

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.

#a-box
r0,0,10,10

#a-circle
c0,0,5

Soon, my build-time converter had an optimized transform for all elements included in SVGs. On the client, I would 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
Visualizing instances

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.

There are a few more optimizations that I have yet to explore, including:

  • Converting my flat file into binary
  • Using GZIP on the flat file and uncompressing it on the client with the Compression Streams API

Click here learn more about BUSY.