100K Particles in 2ms in the Browser with WebGPU + Three.js TSL — The New Performance Benchmark Set by Compute Shaders and GPGPU
If you've ever built a particle system with WebGL and watched frames stutter the moment you crossed tens of thousands of particles, you know exactly what I'm talking about. Faced with the structural bottleneck of calculating positions on the CPU every frame, copying them to a buffer, and uploading them to the GPU, I eventually accepted "this is the ceiling for WebGL." But in 2026, that assumption has changed. WebGPU allows both physics simulation and rendering to be handled entirely inside the GPU via compute shaders — no CPU involvement — and running 100,000 particles in under 2ms in the browser is now possible on mid-range GPUs and above.
With the release of Safari 26 in September 2025, all major browsers — Chrome, Firefox, Edge, and Safari — now support WebGPU by default, bringing global browser coverage to roughly 70%. There may still be a perception of it as an "experimental technology," but now is the right time to seriously consider production adoption. In particular, if you're a frontend developer with experience in WebGL or the Canvas API, the Three.js TSL bridge makes it possible to get started without learning WGSL from scratch.
Core Concepts
Why WebGPU Is Fundamentally Different from WebGL
WebGL is a web wrapper around OpenGL ES 2.0. It was revolutionary by 2011 standards, but it's out of step with 2020s GPU architectures, and its structure — where a CPU driver validates and translates GPU commands in real time — carries significant overhead. Most importantly, it has no compute shaders to run GPU parallel computation outside the rendering pipeline.
WebGPU brings the design philosophy of Direct3D 12, Metal, and Vulkan to the web.
| WebGL | WebGPU | |
|---|---|---|
| Based on | OpenGL ES 2.0 | D3D12 / Metal / Vulkan |
| Compute shaders | None | Supported |
| CPU overhead | High (driver validation) | Low (explicit control) |
| Multithreaded rendering | Not supported | Supported |
| Shader language | GLSL | WGSL |
| Memory control | Implicit | Explicit |
Compute Shaders — This Is the Key
To be honest, most of WebGPU's other advantages are simply "better than WebGL," but compute shaders are a different dimension entirely. They allow parallel computation on GPU cores completely independent of the rendering pipeline.
Take a particle system as an example: in the traditional WebGL approach, the CPU updates each particle's position every frame and uploads it to GPU memory (VRAM) repeatedly. At 100,000 particles, the CPU simply can't keep up. With compute shaders, the GPU handles the position updates itself. The CPU only needs to send the command: "run the shader."
// WGSL compute shader — particle position update
@group(0) @binding(0) var<storage, read_write> positions: array<vec4f>;
@group(0) @binding(1) var<storage, read_write> velocities: array<vec4f>;
@group(0) @binding(2) var<uniform> params: SimParams;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) id: vec3u) {
let i = id.x;
if (i >= arrayLength(&positions)) { return; }
var pos = positions[i];
var vel = velocities[i];
// Apply gravity
vel.y -= params.gravity * params.deltaTime;
// Update position
pos += vel * params.deltaTime;
positions[i] = pos;
velocities[i] = vel;
}workgroup_size(256): The GPU groups threads into workgroups for execution. This structure processes 256 particles simultaneously on GPU cores — fundamentally different from a CPU for loop.
TSL — You Don't Have to Write WGSL Directly
If WGSL feels unfamiliar and the learning curve feels steep, there's another entry point: Three.js TSL (Three Shader Language). You write GPU code in JavaScript syntax, and TSL compiles it to WGSL/GLSL. I honestly thought at first, "Is this actually running on the GPU?" It is.
import {
Fn, float, instanceIndex, deltaTime, If
} from 'three/tsl';
// Looks like JavaScript, but executes on the GPU
const updateParticles = Fn(() => {
const i = instanceIndex;
const pos = positionBuffer.element(i);
const vel = velocityBuffer.element(i);
vel.y.addAssign(gravityUniform.mul(deltaTime).negate());
pos.addAssign(vel.mul(deltaTime));
positionBuffer.element(i).assign(pos);
velocityBuffer.element(i).assign(vel);
})().compute(PARTICLE_COUNT);TSL (Three Shader Language): A JavaScript-based shader DSL supported in Three.js r171+. It's an abstraction layer that lets you leverage the WebGPU compute pipeline without needing to learn WGSL.
Three.js WebGPURenderer — One Line to Swap the Renderer
The first thing I tried in Three.js r171 was swapping the renderer, and it really was just one line. With WebGL 2 automatic fallback included by default, compatibility concerns are greatly reduced as well.
// Previous WebGL
// import { WebGLRenderer } from 'three';
// const renderer = new WebGLRenderer({ antialias: true });
// Switch to WebGPU — this is all you change
import WebGPURenderer from 'three/addons/renderers/common/WebGPURenderer.js';
const renderer = new WebGPURenderer({ antialias: true });
await renderer.init(); // WebGPU requires async initialization
document.body.appendChild(renderer.domElement);Practical Application
Example 1: GPGPU Particle System (Three.js + TSL)
GPGPU (General Purpose GPU Computing) is an approach that offloads non-rendering computations — like physics or simulation — to the GPU. Here's a 100,000-particle example in a React Three Fiber + TSL structure you can use directly in production.
import * as THREE from 'three';
import WebGPURenderer from 'three/addons/renderers/common/WebGPURenderer.js';
// ※ The import path for StorageBufferAttribute may vary depending on the Three.js version
import StorageBufferAttribute from 'three/addons/renderers/common/StorageBufferAttribute.js';
import { Fn, instanceIndex, deltaTime, storage, float, If } from 'three/tsl';
const PARTICLE_COUNT = 100_000;
async function initGPGPU() {
const renderer = new WebGPURenderer();
await renderer.init();
// scene and camera initialization omitted — same as standard Three.js scene setup
// GPU storage buffers — resident in VRAM, not CPU memory
const positions = new StorageBufferAttribute(
new Float32Array(PARTICLE_COUNT * 4), 4
);
const velocities = new StorageBufferAttribute(
new Float32Array(PARTICLE_COUNT * 4), 4
);
// Set initial data
for (let i = 0; i < PARTICLE_COUNT; i++) {
positions.setXYZW(i,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
1
);
velocities.setXYZW(i,
(Math.random() - 0.5) * 0.1,
Math.random() * 0.2,
(Math.random() - 0.5) * 0.1,
0
);
}
const posStorage = storage(positions, 'vec4', PARTICLE_COUNT);
const velStorage = storage(velocities, 'vec4', PARTICLE_COUNT);
const computeUpdate = Fn(() => {
const i = instanceIndex;
const pos = posStorage.element(i);
const vel = velStorage.element(i);
// Gravity + air resistance
vel.y.addAssign(float(-9.8).mul(deltaTime));
vel.mulAssign(float(0.999));
pos.addAssign(vel.mul(deltaTime));
// Floor collision
If(pos.y.lessThan(-5), () => {
pos.y.assign(-5);
vel.y.assign(vel.y.negate().mul(0.6));
});
posStorage.element(i).assign(pos);
velStorage.element(i).assign(vel);
})().compute(PARTICLE_COUNT);
// Connect compute results directly to the render pipeline without copying
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', positions);
const material = new THREE.PointsMaterial({ size: 0.02, color: 0x88ccff });
const particles = new THREE.Points(geometry, material);
function animate() {
requestAnimationFrame(animate);
renderer.compute(computeUpdate); // Update positions on the GPU
renderer.render(scene, camera); // Render directly from the updated buffer
}
animate();
}| Code Point | Description |
|---|---|
StorageBufferAttribute |
A GPU buffer allocated directly in VRAM. No CPU↔GPU copying |
renderer.compute() |
Executes the compute shader. Operates independently of rendering |
geometry.setAttribute('position', positions) |
Connects compute results to the render pipeline without copying |
deltaTime |
Built-in TSL variable. Essential for frame-rate-independent physics |
Compute shaders aren't all WebGPU has to offer. The shaders themselves have changed significantly too, and writing a dissolve animation in TSL makes that difference tangible.
Example 2: Noise-Based Text Dissolve Animation (TSL)
This is the effect you've been seeing on portfolios and landing pages lately — text that scatters into particles. It can be implemented by combining MSDF fonts (Multi-channel Signed Distance Field — a vector font rendering technique that stays crisp when scaled up) with a TSL noise shader.
import { MeshBasicNodeMaterial } from 'three/webgpu';
import {
Fn, uniform, uv, time, vec4, vec3,
mx_noise_float, smoothstep, mix
} from 'three/tsl';
const createDissolveShader = () => {
const dissolveProgress = uniform(0); // 0 = original, 1 = fully dissolved
const fragmentShader = Fn(() => {
// uv is a built-in TSL node — no need to receive it as a parameter
const noiseScale = uniform(5.0);
const noiseValue = mx_noise_float(
vec3(uv().mul(noiseScale), time.mul(0.5))
);
// Discard pixels based on noise threshold
const threshold = dissolveProgress.add(noiseValue.mul(0.3));
const alpha = smoothstep(threshold, threshold.add(0.1), noiseValue);
// Edge glow effect — burning feel
const edgeGlow = smoothstep(
threshold.sub(0.05), threshold, noiseValue
).mul(vec4(0.3, 0.8, 1.0, 1.0)); // Cyan color glow
return mix(vec4(1, 1, 1, alpha), edgeGlow, edgeGlow.w);
});
const material = new MeshBasicNodeMaterial();
material.fragmentNode = fragmentShader;
material.transparent = true;
return { material, dissolveProgress };
};
// Animate the dissolve progress with GSAP
const { material, dissolveProgress } = createDissolveShader();
gsap.to(dissolveProgress, {
value: 1,
duration: 2,
ease: 'power2.inOut'
});uniform: A variable that passes values from the CPU to the GPU shader. Creating one with TSL's
uniform()means that changing.valuein JavaScript updates the shader parameter in real time.
Pros and Cons
Advantages
| Item | Details |
|---|---|
| Dramatic performance gains | Based on byteiota benchmarks, roughly 10x more drawable objects in the same scene compared to WebGL (15,000 objects at 15FPS → 200,000 objects at 60FPS). Results vary by scene composition and hardware |
| Compute shaders | Physics and AI computation is fully offloaded from the main thread, preserving UI responsiveness |
| Zero-copy rendering | Compute results in VRAM are used directly for rendering without any CPU copy |
| Multithreaded command recording | Draw call overhead for complex scenes is distributed across multiple cores |
| W3C standard | Long-term API stability guaranteed. Low dependency on individual browser vendors |
Disadvantages and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Browser compatibility | ~70% global coverage as of 2026 | Use WebGL 2 automatic fallback (provided by Three.js out of the box) |
| Learning curve | Requires understanding of GPU pipelines, buffer layouts, and shader concepts | Start with TSL, then gradually learn WGSL |
| No benefit for simple scenes | Limited gain when draw calls are few or shaders are simple | Apply only to highly parallel workloads like particles and simulations |
| Immature debugging tools | Small ecosystem of dedicated debuggers beyond Chrome DevTools | Use webgpureport.org to check per-device capabilities; rely on step-by-step logging |
| Mobile support | iOS requires Safari 26+, Android requires Chrome 121+ (Android 12+) | Fallback strategy is essential. Think carefully if mobile is your target |
WGSL (WebGPU Shading Language): The official shader language for WebGPU. Inspired by Rust syntax, it has strong type safety and differs significantly from GLSL in syntax. If you use Three.js TSL, you don't need to write it directly.
The Most Common Mistakes in Practice
-
Writing WebGPU initialization as synchronous code —
renderer.init()returns a Promise. If you set up the scene withoutawait, draw calls fire before the renderer is ready and fail silently. After making this mistake once, you immediately grasp how critical it is to guarantee initialization order withasync/await. -
Keeping a structure that uploads GPU buffers from the CPU every frame — If you keep calling
geometry.attributes.position.needsUpdate = truethe WebGL way, you lose all the benefits of WebGPU. The key is to design from the start so animation data lives in VRAM viaStorageBufferAttributeand is updated only by compute shaders. -
Assigning too many threads to a single workgroup — Arbitrarily inflating the workgroup size, like
@compute @workgroup_size(1024), exceeds limits on some GPUs. Most GPUs recommend 256–512 threads per workgroup, and you can check the per-device limit withdevice.limits.maxComputeInvocationsPerWorkgroup.
Closing Thoughts
100,000 particles is a realistic starting point today, and with workgroup tuning and instancing, 1,000,000 is a fully achievable goal. The Three.js r171 + TSL combination is significantly lowering that barrier to entry, and I'm already applying it in some production projects.
Even if you don't have a project that needs large-scale particles right now, starting small today means you'll be able to respond much faster when performance issues arise later.
-
Check browser compatibility first — Visit webgpureport.org to see at a glance what WebGPU features and limits your current device supports. If you're on Chrome 113+ or Safari 26+, you can start right away.
-
Set up a blank WebGPU scene with Three.js r171 — Install with
pnpm add three, then try getting a scene up withWebGPURenderer. If you have existing Three.js experience, swapping the renderer is all it takes to get your first WebGPU experience. -
Take Wawa Sensei's GPGPU Particle Course — A free course that walks through React Three Fiber + TSL + WebGPU GPGPU step by step. If you're new to compute shaders, this order is the most natural path.
References
- WebGPU API | MDN Web Docs
- WebGPU: The Complete Guide to Modern Graphics and Compute on the Web (2026) | explainx.ai
- WebGPU 2026: 70% Browser Support, 15x Performance Gains | byteiota
- WebGPU Hits Critical Mass: All Major Browsers Now Ship It | webgpu.com
- WebGL vs WebGPU: The Performance Gap | Medium
- What's New in Three.js (2026): WebGPU, New Workflows & Beyond | utsubo.com
- Unlock GPU computing with WebGPU — WWDC25 | Apple Developer
- Particles, Progress, and Perseverance: A Journey into WebGPU Fluids | Codrops
- Field Guide to TSL and WebGPU | Maxime Heckel
- Interactive Galaxy with WebGPU Compute Shaders | Three.js Roadmap
- WebGPU Unleashed: A Practical Tutorial
- WebGPU Support | Babylon.js Documentation
- GPGPU particles with TSL & WebGPU | Wawa Sensei
- WebGPU | Can I use
- Awesome WebGPU | GitHub