Using the Browser’s <canvas> for Data Compression
Summary
Before modern APIs, JavaScript could compress data by encoding it into a PNG image via canvas, leveraging the browser's built-in compression.
You can compress data in old browsers using PNGs
Developers can now access data compression in older web browsers by using a clever workaround that leverages the browser's built-in PNG image compression. This technique is useful for applications like storing Single-Page Application (SPA) state in a URL hash, where minimizing data size is critical.
Until the Compression Streams API became widely available in May 2023, JavaScript front ends lacked a standard way to compress data. While modern browsers now support this API, older ones do not. The new method exploits the fact that all browsers automatically compress PNG image data.
The canvas element is the key
The technique works by converting arbitrary binary data into pixel data on an HTML <canvas> element. The browser then compresses this pixel data when the canvas is exported as a PNG image using the toDataURL() method.
Even with the overhead of PNG headers and checksums, the resulting compressed file is often smaller than the original raw data. The process involves packing bytes into an image's red, green, and blue color channels.
- Data is loaded into a 1-pixel-high canvas.
- The alpha channel is set to fully opaque to ensure cross-browser consistency.
- The canvas is converted to a PNG data URL, and the base64-encoded image data is extracted.
How the compression function works
The provided compress() function takes a Uint8Array and returns a base64 string. It first converts the data array and prepends a byte indicating how many data bytes are in the final pixel (1-3). It then writes the data into a canvas's image data, skipping the alpha channel.
Finally, it calls canvas.toDataURL("image/png") and strips the data URL prefix to return just the base64-encoded PNG data. This string is the compressed output.
Decompression requires async handling
Decompression is an asynchronous operation because it requires loading an image. The decompress() function returns a Promise that resolves to the original Uint8Array.
It creates an <img> element from the base64 string, waits for it to load, then draws it onto a canvas. It reads back the pixel data, filters out the alpha channel, and slices the array to remove the header byte and any padding, reconstructing the original data.
Why this workaround matters
Before this technique, developers had few good options for client-side compression in older browsers. Porting a compression library to JavaScript or WebAssembly is possible, but using the browser's own optimized implementation is more efficient.
This method also solves another historical JavaScript problem: encoding arbitrary binary data to base64. The built-in btoa() and functions only work on valid UTF-16 strings, not all Uint8Array objects. The canvas-to-PNG process handles truly arbitrary byte sequences.
The author notes that a test file was created to validate the code, and that Large Language Models (LLMs) were used only to help write that test harness. All other code and the article's content were written without AI assistance.
Related Articles
Arcjet reaches v1.0, promises stable security for JavaScript apps
Arcjet's JavaScript SDK v1.0 is now stable, offering embedded AI security for attack detection and spam prevention directly in code.
How Timsort Algorithm Works
Timsort is a fast hybrid sorting algorithm that combines merge sort and insertion sort. It divides data into runs, sorts them with insertion sort, then merges them efficiently. It's optimized for real-world data, often running in linear time on partially sorted inputs.
Stay in the loop
Get the best AI-curated news delivered to your inbox. No spam, unsubscribe anytime.
