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:
- 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.
- Fast GPU Uploads: Uploading a standard HTML image
to the GPU requires the browser to copy memory inside the main thread.
An
ImageBitmapis already stored in a graphics-friendly format, making the transfer to the GPU incredibly fast.