Rain streaking across the screen. Sparks flying from a blacksmith's anvil. Dust kicking up as a tractor plows a field. Particle systems turn a flat browser game into something that feels alive. And with the right approach, you can run thousands of particles at 60fps in a browser tab.
A particle system is a collection of small, short-lived objects (particles) that are emitted from a source, move according to simple rules, and die after a set lifetime. Each particle has position, velocity, color, size, and age. The emitter controls spawn rate, direction spread, initial velocity range, and lifetime.
The magic of particles is emergence — simple rules on individual particles produce complex, organic-looking effects when thousands of them interact. No two frames look identical, which is what makes rain look like rain instead of a repeating sprite animation.
For small particle counts (under 500), Canvas 2D is perfectly fine. Draw colored circles with ctx.arc(), apply alpha fade-out based on age, and you've got campfire sparks or floating pollen.
For serious particle effects — rain across the entire screen, dense fog, explosion debris — you need WebGL. The GPU can transform and render tens of thousands of point sprites per frame without breaking a sweat, while the CPU would choke on the same workload through Canvas 2D.
| Approach | Max Particles (60fps) | Best For |
|---|---|---|
| Canvas 2D circles | ~300-500 | Simple sparkles, dust motes |
| Canvas 2D images | ~100-200 | Textured particles (leaves, snowflakes) |
| WebGL point sprites | ~50,000+ | Rain, fire, dense effects |
| WebGL instanced quads | ~20,000+ | Textured, rotatable particles |
Every particle system follows the same lifecycle:
dt, interpolate color/size/alpha based on age ratio.splice() — swap the dead particle with the last alive particle and decrement your count.The number one performance killer in JavaScript particle systems is garbage collection. If you new Particle() every spawn and let dead particles get GC'd, you'll see frame stutters every few seconds when the collector runs.
Instead, pre-allocate a fixed-size array of particle objects at startup. "Spawning" means resetting an inactive particle's properties. "Killing" means marking it inactive. Zero allocations, zero GC pressure, buttery smooth.
Emit thin vertical lines (or stretched point sprites) from above the viewport. Gravity pulls them down fast. Short lifetime (0.5–1s). Color: white-blue at low alpha. Add occasional splash particles on impact at y = ground that expand outward and fade quickly. Wind: add a constant horizontal velocity component.
Emit from a small area at the base. Initial velocity: upward with random spread. Color interpolation: yellow → orange → red → transparent over the particle's life. Size: starts medium, shrinks. Add additive blending (gl.blendFunc(gl.SRC_ALPHA, gl.ONE)) for that glowing look. Emit smoke particles above the flame zone: gray, slow-rising, expanding, longer lifetime.
Burst emit (all at once, not continuous) when something impacts the ground. Spread: hemisphere upward from impact point. Color: brown/tan. Size: small, growing slightly. Gravity: low (they float). Alpha: fade out over 1–2 seconds. Good for footsteps, tilling soil, and harvesting.
Emit in a circle or spiral pattern. No gravity. Color: cycle through hue over lifetime. Size: pulsing (use sin(age * frequency)). Trail: store previous positions and draw connecting lines or render multiple fading copies. This is where globalCompositeOperation = "lighter" in Canvas 2D really shines.
For maximum performance, move the entire particle simulation to the GPU. Store particle data in vertex buffer attributes (position, velocity, birth time, lifetime), and let the vertex shader compute the current position each frame based on elapsed time. The CPU only needs to upload the current time as a uniform — zero per-particle CPU work.
This technique, sometimes called "stateless particles," works for effects with predictable physics (gravity, constant wind). For effects that need collision detection or dynamic forces, keep the simulation on the CPU and upload updated positions each frame via bufferSubData().
FarmHeart features dynamic weather, crop effects, and seasonal particles — all in your browser.
Play FarmHeart Now