Canvas 2D is fine for a few hundred particles, but when you want thousands — even millions — of particles moving independently, you need the GPU. In this article, we’ll build a custom particle system using raw WebGL and GLSL shaders to handle the heavy lifting. Fully hardware-accelerated, buttery smooth.
Why WebGL for Particles?
WebGL allows:
- Rendering 10,000+ particles at 60fps
- Parallel computation via vertex shaders
- Full control over physics, color, size, and trails
Step 1: Set Up a Basic WebGL Context
Start by creating a WebGL rendering context:
const canvas = document.getElementById('glcanvas');
const gl = canvas.getContext('webgl');
if (!gl) {
  alert("WebGL not supported");
}
Step 2: Define the Vertex Shader for Particle Positions
Each particle is just a single point processed by the GPU:
const vertexShaderSource = 
  attribute vec2 a_position;
  uniform float u_time;
  void main() {
    float moveX = sin(u_time + a_position.y) * 0.2;
    float moveY = cos(u_time + a_position.x) * 0.2;
    gl_Position = vec4(a_position + vec2(moveX, moveY), 0, 1);
    gl_PointSize = 2.0;
  }
Step 3: Create the Fragment Shader for Coloring
The fragment shader controls how each particle looks:
const fragmentShaderSource = 
  precision mediump float;
  void main() {
    gl_FragColor = vec4(1, 0.5, 0.0, 1); // Orange particles
  }
Step 4: Initialize Buffers and Animate
Load particle positions into a buffer and draw them each frame:
function createShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  return shader;
}
function createProgram(gl, vertexShader, fragmentShader) {
  const program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  return program;
}
const vertices = new Float32Array(10000 * 2).map(() => Math.random() * 2 - 1);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const vShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = createProgram(gl, vShader, fShader);
gl.useProgram(program);
const positionLocation = gl.getAttribLocation(program, "a_position");
const timeLocation = gl.getUniformLocation(program, "u_time");
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
function render(time) {
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.uniform1f(timeLocation, time * 0.001);
  gl.drawArrays(gl.POINTS, 0, 5000);
  requestAnimationFrame(render);
}
requestAnimationFrame(render);
Pros and Cons
✅ Pros
- Extreme performance for huge particle counts
- Highly customizable physics and visuals
- Direct control over GPU rendering
⚠️ Cons
- Steeper learning curve than Canvas 2D
- Debugging shaders can be tedious
- Cross-browser quirks, especially on mobile
🚀 Alternatives
- Three.js: Easier abstraction layer over WebGL
- PixiJS: 2D renderer with fast particle support
- regl: Functional, minimal WebGL abstraction
Summary
When you need insane particle counts or dynamic, real-time visuals in the browser, WebGL is the go-to. By moving particle position calculations onto the GPU, you unleash the raw parallel power of graphics hardware—and the browser becomes your playground.
If this was useful, you can support me here: buymeacoffee.com/hexshift
 
 
    
Top comments (1)
Apply your GLSL skills to the V Shader Hackathon, running until 22 May 2025!
Create unique Shader art to win up to $1000 :)
How to join: medium.com/vsystems/13-26-april-cr...
Any questions? Join our community!