For years, compresspng.com was part of our daily routine. You dropped a PNG onto the page, the site compressed it right away, and a smaller file came back a moment later. The site was reliable and did exactly what we needed. 

Over time, small frictions added up. Compressed files used to download with a -min suffix, such as photo-min.png, so the smaller copy was easy to pick out at a glance. Compresspng.com dropped the suffix and the compressed files started arriving with the same name as the original, which meant renaming every file by hand or letting the download overwrite the original version. Compresspng.com also introduced a button you had to press to start the compression processes, where it used to compress the moment the file was uploaded. Compressing a JPEG requires you to visit an entirely different site, compressjpeg.com, which is essentially a cloned version of compresspng.com. A companion post, Compress Popular Image Types and PDF Files with Our New Compressor Tool, covers the full list of frustrations and what the compressor does.

The annoyances are what kicked off the project, but the interesting part is what’s under the hood. Modern browsers can handle a surprising amount of work that we reflexively hand to a server. Could we build a single tool that automatically compressed PNG, JPEG, WebP, SVG and PDF files entirely in the browser? Can we do this all on a single page? The following is a technical story of how we build our own compression tool.

The realization: there is no server

Before writing a line of code, I wanted to understand how the original tool worked. I assumed the usual SaaS shape: you upload the file, a backend compresses the file, and you download the result. If that assumption was correct, rebuilding the tool would mean standing up real infrastructure: an image pipeline, storage, and a processing queue.

So I opened the browser developer tools, switched to the Network tab, and dropped a PNG onto compresspng.com.

Nothing was uploaded. There was no multipart POST, and no bytes left my computer. The compression was running entirely in the browser.

That observation reframed the whole project. We did not need a backend, an image pipeline, or a storage bucket. We needed a web page carrying the right JavaScript.

Why a WordPress plugin

Our internal site already runs on WordPress, and we wanted the compressor to live on an ordinary page that anyone on the team could bookmark. So the deliverable became a plugin that exposes a shortcode,

Compress your images, SVGs, and PDFs directly in your browser. Drop your files below and they will be compressed automatically. Your files never leave your device.

Drag & drop files here

or

PNG, JPEG, WebP, SVG, PDF • Up to 20 files • 100% in-browser

, which you can drop onto any page or post.

The surprising part is how little the PHP does. The plugin performs no server-side image processing at all. The PHP has one job: register the shortcode, then hand the browser a stylesheet, a script, and an empty container to render into.

The hard part: every format compresses differently

I started out imagining a single compress() function, but no such function exists. “Compressing an image” turns into five different operations depending on the file type, and getting each operation right took most of the development time. The plugin supports PNG, JPEG, WebP, SVG, and PDF, and each format needed its own approach.

JPEG and WebP: the browser already does the work

JPEG and WebP cost us nothing. The HTML Canvas API can re-encode an image at any quality level without a library at all. You draw the image onto a <canvas> and read the image back as a blob:

function compressJPEG(canvas, quality) {
  return canvasToBlob(canvas, 'image/jpeg', quality / 100);
}

The quality argument, a value from 0 to 1, comes straight from a slider in the interface. Those few lines are the entire JPEG and WebP implementation, and the browser’s built-in encoder handles the rest.

PNG: the format that needs a real library

PNG comes with a trap. The Canvas API accepts a quality parameter, but the API ignores that parameter for PNG. PNG is lossless by design, so canvas.toBlob('image/png') re-encodes the same pixels and usually produces a file no smaller than the original.

The fix is palette quantization: reducing a 24- or 32-bit image to an indexed PNG-8 with a limited colour palette. On screenshots and interface graphics the change is nearly invisible to the eye, and the file often drops below half its original size.

The browser cannot perform palette quantization on its own, so PNG is the only format that pulls in a dependency: UPNG.js (the encoder, written by the author of Photopea) and pako (a JavaScript implementation of zlib/DEFLATE that UPNG.js depends on). The slider sets the size of the palette, anywhere from 2 to 256 colours.

function compressPNG(canvas, quality) {
  if (quality >= 100) return canvasToBlob(canvas, 'image/png'); // lossless re-save

  return loadUPNG().then(function (UPNG) {
    var w = canvas.width, h = canvas.height;
    var rgba   = canvas.getContext('2d').getImageData(0, 0, w, h).data.buffer;
    var colors = Math.max(2, Math.round(256 * quality / 100)); // 2–256 colors
    return new Blob([UPNG.encode([rgba], w, h, colors)], { type: 'image/png' });
  });
}

SVG: a document, not an image

An SVG is XML rather than pixels, so none of the canvas machinery applies. The savings come from deleting the clutter that design tools leave behind. Illustrator, Figma, Inkscape, and Sketch all pad their exports with editor metadata, comments, hidden layers, and namespaces that the browser never reads.

For SVG I did not reach for a library at all, because the browser already ships an XML parser. The optimizer parses the SVG with DOMParser, walks the tree, removes the clutter, and serializes the SVG back out with XMLSerializer:

// Drop whole elements that are pure metadata or a security risk
var REMOVE_ELEMS = ['metadata', 'title', 'desc', 'script'];

// Drop editor-specific attributes and namespaces
var EDITOR_ATTR_PREFIXES = ['inkscape:', 'sodipodi:', 'sketch:', 'xmlns:dc', 'xmlns:cc', 'xmlns:rdf'];

// ...and strip any inline event handlers (onclick, onload, ...) while we're at it
if (name.indexOf('on') === 0) node.removeAttribute(name);

Stripping <script> and on* handlers saves a little size and also closes a security hole, because an SVG can carry executable JavaScript and we do not want to hand that script back to someone who thinks they only compressed a logo. On real designer exports the cleanup typically saves 20 to 50 percent with no visible change. The quality slider has no effect on SVG, because SVG compression is always a structural cleanup.

PDF: the ambitious format

A PDF is a container that bundles text, vectors, fonts, and embedded raster images together, so you cannot re-encode the whole document in one pass. The biggest and easiest win is the embedded photos, which usually sit inside the document as untouched JPEGs.

Using pdf-lib, we load the document, walk the document’s internal object graph, find every embedded JPEG, recompress each JPEG with the same Canvas trick we use for standalone files, and swap the smaller version back in. Everything else stays exactly as it was, including the text, vector art, links, bookmarks, and form fields.

The decisions that made the compressor good, not just functional

Getting each format to compress was only the baseline. A handful of deliberate choices are what made the team prefer the plugin over the original site.

Auto-compress on drop

Auto-compression is the feature the whole project was built around. The moment files land in the dropzone, whether dragged in or chosen through the file picker, compression begins. There is no Start button, because a Start button would recreate the very friction we set out to remove.

Load libraries only when they’re needed

The vendor libraries are not small. pdf-lib alone runs to about half a megabyte. Forcing someone who only wants to compress a JPEG to download a PDF engine they will never touch would be wasteful, so nothing loads up front. Each library is fetched once, the first time a file needs that library, by injecting a <script> tag on demand:

function loadScript(src) {
  return new Promise(function (resolve, reject) {
    var s = document.createElement('script');
    s.src = src;
    s.onload = resolve;
    s.onerror = function () { reject(new Error('Failed to load ' + src)); };
    document.head.appendChild(s);
  });
}

// pdf-lib is only ever fetched the first time someone drops a PDF
function loadPDFLib() {
  if (window.PDFLib) return Promise.resolve(window.PDFLib);
  return loadScript(vendor.pdflib).then(function () { return window.PDFLib; });
}

The payoff is a lean default. JPEG, WebP, and SVG need no external libraries at all. PNG pulls in pako and UPNG.js, PDF pulls in pdf-lib, and “Save All” pulls in JSZip.

Never hand back a bigger file

Compression does not always win. An already-optimized JPEG re-encoded at high quality can come out larger than the file we started with. So after every operation we compare the two sizes, and if the compressed result is not smaller, we keep the original:

if (blob.size >= entry.originalSize) {
  entry.compressed     = entry.file;          // keep the original; it was already smaller
  entry.compressedSize = entry.originalSize;
} else {
  entry.compressed     = blob;
  entry.compressedSize = blob.size;
}

One at a time, and everything bundled locally

Files are compressed one at a time rather than all at once. Decoding twenty full-resolution images into canvases simultaneously will push a browser tab out of memory, so a promise chain runs the jobs in sequence and keeps memory flat at the cost of a little wall-clock time.

The vendor libraries are also bundled with the plugin rather than pulled from a CDN. For an internal tool, bundling wins on two fronts: the plugin keeps working even when a CDN has a bad day, and the page makes no third-party requests, which matters when the whole promise is that your files never leave your device.

Save All as a single zip

Downloading twenty files one click at a time is its own kind of friction. When more than one compressed file is ready, JSZip bundles the files into a single compressed-files.zip.

The packages and why they’re needed

We wrote the interface, the SVG optimizer, the format routing, and all of the glue, but we did not reinvent zlib or write a PDF parser from scratch. Four mature, permissively licensed libraries handle the parts the browser cannot do on its own, and every one of those libraries loads on demand, the first time a file calls for that library.

pako: DEFLATE in the browser

PNG compression is DEFLATE all the way down, and the browser does not expose a DEFLATE encoder to JavaScript. pako is the de facto port of zlib to JavaScript and the dependency that UPNG.js is built on, so pako loads alongside UPNG.js every time we compress a PNG.

UPNG.js: the lossy PNGs the Canvas API refuses to make

UPNG.js solves the PNG problem from earlier: the canvas can re-encode a PNG but cannot make the PNG any smaller. UPNG.js handles the palette quantization in pure JavaScript, reducing the image to an indexed PNG of between 2 and 256 colours.

pdf-lib: read, modify, and re-save a PDF with no server

Recompressing the images inside a PDF means parsing the document’s object structure, finding the image streams, replacing their bytes, and writing a valid PDF back out. Writing a PDF parser is a hard problem and not something worth hand-rolling, and pdf-lib handles the whole job in the browser. pdf-lib is also the heaviest dependency by a wide margin, at roughly half a megabyte, which is the single biggest reason the on-demand loading scheme exists.

What we ended up with

The entire compressor is a thin layer of PHP, a few hundred lines of our own JavaScript, and four small libraries that load only when a given format needs them. There is no backend, no upload step, and no image pipeline to keep running. The browser handled most of the work. The two remaining jobs, lossy PNG encoding and editing images inside a PDF, each came from a single well-tested library.

For a tool the whole team now reaches for every day, that is a small amount of code to own and maintain. You can try it here.  Let us know how you like it!

The Extra Click that Made Us Build Our Own Image Compressor