Frame-rate-independent particle emission: a small accumulator pattern

I kept seeing emitters that looked correct at 60 Hz but became visibly thinner at lower frame rates. The root cause was treating a spawn rate as “particles per frame” instead of “particles per second.”

This small accumulator pattern has been reliable:

const particlesPerSecond = 48;
let emissionCarry = 0;

function updateEmitter(deltaSeconds) {
  const safeDelta = Math.min(deltaSeconds, 1 / 15);
  emissionCarry += particlesPerSecond * safeDelta;

  const spawnCount = Math.floor(emissionCarry);
  emissionCarry -= spawnCount;

  for (let i = 0; i < spawnCount; i++) {
    spawnParticle();
  }
}

Carrying the fractional remainder is important. If it is discarded every frame, low rates such as 2.5 or 7.5 particles per second drift substantially. Clamping the delta also prevents a backgrounded browser tab from returning with a huge one-frame burst.

For effects that must be deterministic, I would put the simulation behind a fixed-step accumulator and render from the current state. For most short gameplay effects, elapsed seconds plus the fractional emission carry is enough.

I wrote a longer explanation while documenting the Three.js particle runtime used by NixieFX: NixieFX Three.js Runtime — Guide & API Reference

The same pattern is independent of the editor/runtime—rates stay in units per second, the game loop supplies elapsed seconds, and each emitter owns its fractional carry.