Skip to content

Performance

Particle systems can get expensive quickly. The cost is usually in one of a few places: too many particles alive at once, expensive per-particle work each frame, or specific features that have steep per-emit costs (Model cloning, Beam GraphBlender, real-time shadows).

This chapter walks through the common causes of frame-rate problems with the plugin, ordered roughly by how often each one is the actual culprit.

The first thing to check: how many particles are alive at once?

Section titled “The first thing to check: how many particles are alive at once?”

The dominant cost in any particle system is the count of active particles. For a continuously-emitting emitter, the average number alive at any moment is approximately:

active particles ≈ Rate × average Lifetime

A Rate = 30 emitter with Lifetime = 1.0,2.0 has roughly 30 × 1.5 = 45 active particles on average. A Rate = 100 emitter with Lifetime = 5.0 has 500. A scene with twenty such emitters has 10,000 active particles every frame.

Each active particle costs:

  • One per-frame motion update (CFrame computation)
  • One per-frame graph evaluation per active graph property (Speed, Size, Color, Transparency, etc.)
  • One per-frame property write to the duplicated visual (or per-frame blend math for Beam)

For ten thousand active particles with five active graphs each, that’s 50,000 graph evaluations per frame — roughly the upper end of what the engine handles smoothly on mid-range hardware.

The single most effective performance lever: reduce Rate or Lifetime. Cutting either in half cuts active particle count in half. For most effects, the visual cost of a 50% reduction is much smaller than the perceived cost — viewers don’t count individual particles, they read the overall density and motion.

Mesh particles cost more than Block particles

Section titled “Mesh particles cost more than Block particles”

A Part emitter rendering a primitive shape (Block, Sphere, Cylinder) is cheaper per particle than a MeshPart-based emitter. The engine duplicates simpler geometry faster, and the GPU draws primitive shapes more efficiently than custom meshes.

For high-rate emitters where the visual quality of a primitive is acceptable, prefer primitives. The visual difference between a Block particle and a MeshPart particle at small sizes (under 1 stud) is often invisible on screen.

When a MeshPart is required (a custom-modelled coin, a glass shard with a non-primitive shape), the per-particle cost is real but acceptable for moderate rates. Push past Rate = 50 with a complex MeshPart and you’ll see the cost.

Every Model emit clones the model’s entire subtree. A Model with twenty descendants costs roughly twenty times the duplication of a single Part. With Rate = 30, that’s 600 descendant clones per second going through the engine.

Two ways to reduce this:

  • Reduce descendant count. A fireball Model with eight purely-decorative child Parts can probably get the same visual effect with three or four. Audit the Model for children that don’t contribute to the final look.
  • Use a Part emitter with nested children instead. A Part whose RenderTemplate contains the same set of children as your Model’s PrimaryPart subtree gives you the same composite-particle effect without the Model wrapper, sometimes with cheaper cloning. Trade-off: you lose Model’s Scale graph, but most cases work fine with SizeX/Y/Z on the Part instead.

If the Model truly needs many descendants and a high emit rate, profile in Studio’s MicroProfiler to confirm cloning is the bottleneck before assuming.

Each simulation step, every active Model-particle gets Model:ScaleTo() called on it with the current Scale-graph value. :ScaleTo traverses the entire subtree and multiplies every descendant’s Size, every Mesh size, every Attachment offset. The step granularity is set by TotalKeyFrames (Anim. Steps) — at the default 100 steps over a one-second Lifetime, that’s roughly one or two :ScaleTo calls per frame at 60Hz; at longer Lifetimes the calls thin out proportionally.

For a Model with fifty descendants and thirty active particles, that’s roughly 1,500 internal property updates per simulation step just for scaling. The plugin doesn’t skip-if-unchanged — even a constant-Scale graph still triggers :ScaleTo each step.

If your Scale graph is constant or near-constant, set every keypoint to 1.0 and the call still happens, but the work :ScaleTo does internally is minimal (no actual size changes propagate). The cost is the per-step walk, which scales with descendant count.

For Models where Scale doesn’t need to animate, the lowest-friction optimisation is keeping descendant count small. Lowering TotalKeyFrames also reduces the call rate, at the cost of choppier scale animation.

Beam emitters with multi-state GraphBlender build a fresh NumberSequence and ColorSequence per active beam each frame. The engine reuses precomputed merged times across blends, so the per-blend work is cheap, but the sequence objects themselves are allocations the GC will eventually clean up.

At ordinary beam counts the cost is fine. At very high counts (many tens of GraphBlender beams active at once) the allocations begin to show up in MicroProfiler as GC pressure.

If you can author the same effect with a single static Color and Transparency instead of multi-state GraphBlender, you save the allocation. GraphBlender is the right call when you genuinely need multi-state interpolation; when one state would do, skip it.

Roblox’s lighting engine renders a soft cap of 8–16 dynamic lights per fragment, depending on hardware tier. Beyond that, additional lights silently don’t contribute to the lighting calculation — the engine picks the closest or brightest and ignores the rest.

This means a PointLight emitter at Rate = 30 with Lifetime = 2.0 (60 active lights at any moment) won’t actually look like 60 lights. Most of them won’t render. The cost of the engine trying is real (each one still goes through the visibility check), but the visual won’t match what you’d expect.

For PointLight emitters, keep Rate × Lifetime reasonably small. Rate = 5, Lifetime = 1.0 (5 active lights) is well within budget. Rate = 30, Lifetime = 2.0 is wasteful — most lights won’t render.

For glow effects that need to look like many lights, consider a single PointLight emitter at low Rate combined with bright Color and high Range. One bright light reads as more “presence” than ten dim lights.

A PointLight with Shadows = true adds shadow-rendering cost on top of the per-fragment lighting calculation. Each shadow-casting light affects every nearby surface’s shadow pass.

Roblox enforces a tighter limit on shadow-casting lights than on regular lights — typically 1–4 per fragment depending on hardware. Past that, shadows silently don’t contribute.

For particle-style PointLights — magical orbs, projectile glows, brief flashes — leave Shadows = false. Reach for shadows only on stationary, deliberate PointLights where the cast shadow is part of the effect (a torch with a visible character-shadow on the wall, for instance).

The first time a textured emitter (Part / Beam / Trail / ImageLabel) emits, Roblox loads and decodes the texture asset on demand. The decode takes 50–200 milliseconds — long enough to span a few frames at 60 fps. During that window, particles render with no texture.

This isn’t a frame-rate problem per se, but it shows up as a visible “pop” on the first emission of a textured effect. The fix is PreloadTexture = true, which forces the asset to load before emission. See Texture Pinning.

For high-stakes cinematic moments (the first emission of a critical effect), use PreloadTexture. For ambient effects where the first emission isn’t tightly timed, the pop is usually invisible.

Of all the emitter types, ImageLabel has the lowest per-particle cost. The 2D rendering path doesn’t go through the 3D pipeline — no skinning, no shadow casting, no per-fragment lighting calculations. The cost is mostly in graph evaluation and per-particle property updates.

ImageLabel emitters at Rate = 100 or even Rate = 200 are viable on mid-range hardware. If you have UI-side effects that need to feel dense (damage numbers, score popups, sparkles) and you’re worried about cost, ImageLabel is the cheap option compared to 3D Part emitters.

Roblox players span a wide hardware range — from low-end mobile to high-end PC. An effect that runs smoothly on PC may stutter on mobile. Two approaches to handling this:

The first is conditional emission — your script checks device tier and reduces Rate (or skips emission entirely) on low-end. Roblox provides UserInputService:GetPlatform() and similar; combine with manual quality-tier detection.

The second is lower default budgets — author for the low-end target and accept that high-end devices have headroom. For most games, this is the right call. Players on low-end hardware are the bottleneck; designing for them produces an experience that works everywhere.

Avoid the trap of authoring at full quality on a high-end dev machine and shipping it to mobile players. Test on a mid-range mobile device or use Studio’s emulation tools to catch performance issues early.

Studio’s MicroProfiler (Ctrl+F6) shows per-frame timing breakdowns. Look for:

  • A Part_Icles label or Heartbeat label growing under load — that’s the plugin’s update loop. If it’s the bottleneck, particle count is too high.
  • GarbageCollection spiking — that’s GraphBlender allocations or other per-frame allocations adding up. Reduce GraphBlender beam count.
  • Renderer spiking with high mesh-particle counts — that’s the GPU. Reduce particle count or switch to primitives.

Profile with the actual gameplay scenarios, not just emitter previewing in Studio’s edit mode. Particle costs only matter when stacked with the rest of your game’s frame-time budget.

Type differences is the same-name-different-shape (polymorphism) quick-reference — properties whose data type or meaning varies by emitter type, with a lookup table for the common gotchas.