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.