texture-compression – Pal's Blog

Do not use uncompressed textures

Texture uploading

The standard way to upload a texture to graphics hardware in WebGL is to use texImage2D. It comes from the OpenGL ES glTexImage2D function, which accepts no native multimedia formats but rather a buffer of raw pixel data. That means the WebGL implementation abstracts away an inefficient process of uncompressing images before uploading them to graphics memory.

This under-the-hood decompression takes up CPU cycles when an application loads its assets. Not only that, but uncompressed textures are memory inefficient; for context, a 2048×2048 8-bit RBGA texture takes up a minimum of 16mb graphics texture.

It’s easy to load up many textures and hog hundreds of megabytes in graphics memory. When the GPU memory limit is hit, desktop computers will generally start paging memory to disk, and that causes system-wide graphics pauses lasting for seconds. On mobile devices, the OS kills applications using too much memory immediately to prevent any slowdowns. I’ve noticed iOS Safari will reload the page if it takes too much graphics memory, not excluding the memory used by the browser to render HTML.

Texture compression

I recommend using GPU-compressed texture formats to reduce the resources consumed by an application’s textures. This form of texture compression is designed specifically for hardware-accelerated graphics programs. GPU-compressed formats generally use “block compression”, where blocks of pixels are compressed into a single datapoint. I dive into how you can use them in PixiJS at the end.

The WebGL API provides support for GPU-compressed texture formats through various extensions. A GPU-compressed texture does not get decompressed before being uploaded to graphics memory. Instead, the GPU decompresses texels on-the-fly in hardware when a shader reads the texture. Texture compression is designed to allow “random access” reads that don’t require the GPU to decompress the whole texture to retrieve a single texel.

By using compressed texture formats, a WebGL application can free up CPU cycles spent by the browser decompressing images and reduce the video memory occupied by textures. The latter is especially important on low-end devices or when your application is using a lot of textures.

I built a tool, Zap, to let you generate GPU-compressed textures with no setup!


GPU-compressed textures come with their caveats, like all good things in life!

One trade-off is that GPU-compressed textures take up more disk space than native images, like PNGs, which use more sophisticated compression algorithms. This means downloading a bunch of compressed textures can increasing asset loading time over the network.

GPU-compressed formats are also very platform-dependent. Most graphics cards support only one or two of the several texture compression formats. This is because the decoding algorithm has to be built into the hardware. And so, having a fallback to native images is still necessary; maintaining images in several different compressed formats is also a bunch of homework. PixiJS’ @pixi/compressed-textures allows an application to serve a texture manifest that lists the available formats; then, the texture loader picks the appropriate version based on the device.


“Supercompressed” texture formats like Basis solve the disk size and platform-dependency problems of GPU-compressed formats. A supercompressed texture is an intermediate format served over the network and then transcoded into a supported GPU-compressed format on the client. Basis provides a transcoder build that can be invoked from JavaScript. As a fallback, the Basis transcoder also allows decoding to an uncompressed 16-bit RGB texture.

The basis transcoder is an additional ~500kb of JavaScript / WebAssembly code (without gzip compression). Fetching and executing it adds a tiny overhead when initializing, but that should be worth it if you use more than a megabyte of images. The Basis supercompressed format is still smaller than native formats like PNG, so you might actually save download time on average.

Testing how much GPU memory is saved

If you’ve kept reading to this point, you might be thinking about how to know if using compressed textures is worth it?

I made two sample apps that load the same 600x400px texture 100 times, one using uncompressed textures, and the other using compressed textures. A small canvas is used to reduce the framebuffer’s effect on memory usage. I used PixiJS because PixiJS 6’s @pixi/compressed-textures has out-of-the-box support for compressed formats!

You can open the sample apps in Chrome and open the browser task manager. Note that you might have to wait up to 30 seconds for them to load because Replit seems to throttle the image downlink. To view the GPU memory of each process, you’ll need to enable that column.

The uncompressed sample (above) takes 100mb of GPU memory.

While the compressed sample takes only 30mb − that’s 70% less hogged memory! PixiJS also has to create an HTMLImageElement for each native image texture, and you can see that also affects the main memory usage.

Of course, the trade-off is in the 4-5xed download size of the textures (6mb vs. 25mb). As I mentioned earlier, if you’re downloading more than a megabyte of textures − it’s worth using supercompression to save bandwidth.

PixiJS 6’s @pixi/basis adds support for using Basis textures. To test Basis, I generated a Basis texture from Zap and plugged it into this sample.

The results are similar to that of the compressed texture sample; in this case, PixiJS chose a more compact compressed format (DXT1) than the one I uploaded in the prior sample (DXT5) so GPU memory usage has further decreased.

Moreover, this sample fetches all textures in just 1.7mb of network usage!

Notice the “dedicated worker” row in the task manager. @pixi/basis creates a pool of workers for transcoding so the application UI does not slow down.

Try it out using Zap

Zap is a tool I built to help you get started with texture compression. Traditional tools like Compressonator, NVIDIA’s Texture Tools, PVRTexTool are clunky, OS-specific, and have a steep learning curve. I literally had to install Windows to test out Compressonator, and it was really slow.

Zap is a simple web app that lets you upload a texture to be processed by my server. It supports 10 different compression formats plus the UASTC basis supercompression format. Not only that, it’s free to use (for now 😀).

To use Zap, simply upload a native image and select the compression formats you want. That will redirect you to a permanent link, at which the compressed textures will be available after processing. It may take several seconds on larger images. Note the compressed textures will be deleted after a few days.

PixiJS Compressed Textures Support

PixiJS 6 supports most of the GPU-compressed formats out of the box (exception being BC7). You can use them just like you use native images.

To use the Basis format, you need to import BasisLoader from @pixi/basis, load the transcoder, and register the loader. Then, the PixiJS Loader API can be used in the standard manner:

// Include the following script, if not using ESM
// <script src="https://cdn.jsdelivr.net/npm/@pixi/basis@6.2.2/dist/browser/basis.min.js"></script>

// Load transcoder from JSDeliver
const BASIS_TRANSCODER_JS_URL = 'https://cdn.jsdelivr.net/npm/@pixi/basis@6.2.2/assets/basis_transcoder.js';
const BASIS_TRANSCODER_WASM_URL = 'https://cdn.jsdelivr.net/npm/@pixi/basis@6.2.2/assets/basis_transcoder.wasm';

// Without this, PixiJS can't decompress *.basis files!

// Make sure the BasisLoader is being used!

// Usage:
    .add("your-file.basis", "your-file.basis")
    .load((_, resources) => {
       // Use this texture!
       const texture = resources['your-file.basis'];

Hey there, I’m Shukant and I’m building the future of work at Teamflow, the best virtual office for remote companies. Thanks for visiting my site!