WebGPU hands out its adapter and device through promises (Adapters and Devices), so WebGPURenderer finishes starting in init(). setAnimationLoop() awaits it on its own, but render() and hasFeature() throw until await renderer.init() has resolved (renderAsync() and hasFeatureAsync() are deprecated since r181). The WebGPU capability add-on asks for an adapter up front:
import WebGPU from 'three/addons/capabilities/WebGPU.js';
import { WebGPURenderer } from 'three/webgpu';
console.log('WebGPU.isAvailable():', WebGPU.isAvailable()); // an adapter was granted
const renderer = new WebGPURenderer();
await renderer.init(); // render() and hasFeature() throw before
console.log('backend:', renderer.backend.isWebGPUBackend ? 'WebGPU' : 'WebGL 2',
'| timestamp-query:', renderer.hasFeature('timestamp-query'),
'| shader-f16:', renderer.hasFeature('shader-f16'));
window.__done = true;Output
WebGPU.isAvailable(): true backend: WebGPU | timestamp-query: true | shader-f16: true
Without WebGPU, init() falls back to WebGL 2 with a warning, so check renderer.backend.isWebGPUBackend when a feature needs the real thing. With forceWebGL: true the same checks reported WebGL 2, still with timestamp-query (via EXT_disjoint_timer_query_webgl2). Browser Support in 2026 covers browser support.
<!doctype html>
<style>
body { margin: 0; background: #f5f1e8; font: 12px system-ui, sans-serif; color: #263238; }
.row { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-start; padding: 4px; }
canvas { display: block; }
ul { list-style: none; margin: 0; padding: 8px; background: #fff; min-width: 290px; }
li { padding: 2px 0; font-family: monospace; }
.yes::before { content: '✔ '; color: #3f7d3a; }
.no::before { content: '✘ '; color: #b5452f; }
</style>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.webgpu.js",
"three/webgpu": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.webgpu.js",
"three/tsl": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.tsl.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<div class="row" id="row"><ul id="list"></ul></div>
<script type="module">
import * as THREE from 'three/webgpu';
import WebGPU from 'three/addons/capabilities/WebGPU.js';
const list = document.getElementById('list');
const report = (text, ok) => list.appendChild(Object.assign(document.createElement('li'), { textContent: text, className: ok ? 'yes' : 'no' }));
// The capability add-on asks for an adapter up front
report(`WebGPU.isAvailable(): ${WebGPU.isAvailable()}`, WebGPU.isAvailable());
const renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(260, 240);
document.getElementById('row').prepend(renderer.domElement);
// render() and hasFeature() throw until init() has resolved; without WebGPU it falls back to WebGL 2
await renderer.init();
const webgpu = renderer.backend.isWebGPUBackend;
report(`backend: ${webgpu ? 'WebGPU' : 'WebGL 2 (fallback)'}`, webgpu);
for (const feature of ['timestamp-query', 'shader-f16', 'float32-filterable', 'texture-compression-bc', 'texture-compression-astc']) {
const has = renderer.hasFeature(feature);
report(`hasFeature('${feature}'): ${has}`, has);
}
const scene = new THREE.Scene();
scene.background = new THREE.Color(webgpu ? '#eef5ea' : '#fbf1e6');
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.5));
const sun = new THREE.DirectionalLight('#ffffff', 2);
sun.position.set(2, 3, 4);
scene.add(sun);
const book = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.9, 0.14), new THREE.MeshStandardMaterial({ color: '#1f5f8b', roughness: 0.5 }));
scene.add(book);
const camera = new THREE.PerspectiveCamera(35, 260 / 240, 0.1, 50);
camera.position.set(0, 0.2, 2);
camera.lookAt(0, 0, 0);
renderer.setAnimationLoop((t) => { // setAnimationLoop would also await init() by itself
book.rotation.y = t / 1500;
renderer.render(scene, camera);
});
</script>