Stream Textures with ImageBitmapLoader in Three.js

Loading large textures in Three.js can cause noticeable frame rate drops and stuttering on the main thread. This article explains how to use ImageBitmapLoader to asynchronously decode images on a background thread, enabling progressive texture streaming that keeps your Three.js application running at a smooth and consistent frame rate.

The Problem with Traditional Texture Loading

When you use the standard TextureLoader in Three.js, the browser decodes the image data on the main execution thread. For large or numerous textures, this decoding process blocks the main thread, causing temporary freezes (jank) and dropping the frame rate.

ImageBitmapLoader solves this by utilizing the browser’s createImageBitmap() API. This API offloads the expensive image decompression and decoding work to a background worker thread. When the main thread receives the completed ImageBitmap, it is already decoded and ready to be uploaded directly to the GPU, resulting in zero-jank rendering.

Implementing ImageBitmapLoader

To implement progressive loading, you first display a low-resolution placeholder texture, load the high-resolution texture in the background using ImageBitmapLoader, and swap the textures once the download and decode processes are complete.

Here is the step-by-step implementation:

import * as THREE from 'three';

// 1. Create your mesh with a low-resolution placeholder texture
const textureLoader = new THREE.TextureLoader();
const lowResTexture = textureLoader.load('path/to/low-res.jpg');

const material = new THREE.MeshStandardMaterial({
    map: lowResTexture
});
const mesh = new THREE.Mesh(new THREE.BoxGeometry(), material);

// 2. Initialize the ImageBitmapLoader for the high-res texture
const bitmapLoader = new THREE.ImageBitmapLoader();

// Configure the loader to match Three.js texture coordinates (Y-axis flipping)
bitmapLoader.setOptions({ 
    imageOrientation: 'flipY',
    premultiplyAlpha: 'none'
});

// 3. Load the high-resolution image asynchronously
bitmapLoader.load(
    'path/to/high-res.jpg',
    function (imageBitmap) {
        // Create a texture using the decoded ImageBitmap
        const highResTexture = new THREE.CanvasTexture(imageBitmap);
        
        // Swap the low-res texture with the high-res texture
        const oldTexture = material.map;
        material.map = highResTexture;
        material.needsUpdate = true;

        // Dispose of the old placeholder texture to free memory
        oldTexture.dispose();
    },
    function (xhr) {
        // Optional: Track progress
        console.log((xhr.loaded / xhr.total * 100) + '% loaded');
    },
    function (err) {
        console.error('An error occurred loading the ImageBitmap:', err);
    }
);

Why This Keeps Framerates Smooth

By using this pattern, you gain two major performance advantages:

  1. Background Decoding: The heavy computational cost of parsing and unzipping JPG or PNG files happens entirely off the main thread. User interactions, animations, and physics loops continue to run uninterrupted.
  2. Fast GPU Uploads: Uploading a standard HTML image to the GPU requires the browser to copy memory inside the main thread. An ImageBitmap is already stored in a graphics-friendly format, making the transfer to the GPU incredibly fast.