Quick answer: The most common causes of low FPS in Construct 3 are too many active particles, oversized or non-power-of-2 sprite sheets, collision checks running every tick against large object groups, high object counts without cleanup, and excessive draw calls from layers and blend modes. Profile first with the built-in debugger to identify which bottleneck applies.
Here is how to fix low FPS and performance lag in Construct 3. Your game runs fine for the first few seconds, then the frame rate drops to 30 FPS, 20 FPS, or worse. Or maybe it was never smooth to begin with — even a simple scene stutters on mobile devices. Construct 3 runs on WebGL and JavaScript, which means performance is directly tied to how efficiently you use textures, objects, and event logic. Let us walk through the most impactful optimizations.
The Symptom
The game runs below 60 FPS. You might notice choppy scrolling, delayed input response, animation stuttering, or visible frame skipping. On mobile devices, the performance is significantly worse than on desktop. The Construct 3 debugger (available in preview mode) shows high CPU or GPU time, or an object count that keeps climbing.
In some cases, performance starts fine and degrades over time. This points to a memory leak — objects being created but never destroyed, particles accumulating, or event logic that grows more expensive as the game progresses.
In other cases, performance is consistently poor from the start. This usually indicates a rendering bottleneck: too many draw calls, textures that exceed the GPU’s comfortable working set, or effects applied to large areas of the screen.
What Causes This
There are seven common causes of poor performance:
1. Too many particles. The Particles plugin is one of the most expensive objects in Construct 3. Each particle is a separately rendered quad with its own transform, opacity, and color. A single Particles object emitting 100 particles per second with a 5-second lifetime means 500 active particles — 500 separate draw operations. Multiple emitters compound this rapidly.
2. Large sprite sheets not power-of-2. WebGL works most efficiently with textures whose dimensions are powers of 2 (256, 512, 1024, 2048). Non-power-of-2 (NPOT) textures may be padded by the GPU driver, wasting VRAM. Very large sprite sheets (4096x4096 or bigger) can exceed mobile GPU limits entirely, causing software fallback rendering.
3. Collision checks on every tick. A condition like “Sprite is overlapping EnemyBullet” in a regular (non-triggered) event runs every single tick for every instance of both object types. With 50 player bullets and 50 enemy bullets, that is 2,500 overlap tests per tick — at 60 ticks per second, that is 150,000 tests per second.
4. “Every tick” vs timer triggers. Events that use “Every tick” run 60 times per second. If the logic inside is expensive (like spawning objects, running loops, or performing string operations), it adds up. Many of these can be replaced with “Every X seconds” triggers that run at a lower frequency.
5. Unmanaged object counts. Bullets, effects, and spawned decorations that leave the screen but are never destroyed keep consuming CPU for event processing and GPU for rendering (even off-screen objects participate in some engine overhead). Object counts that grow without bound are the most common cause of performance degradation over time.
6. WebGL renderer and GPU issues. Some GPUs, especially integrated graphics on older laptops and low-end mobile chipsets, are blacklisted by browsers for WebGL. The browser falls back to software rendering, which is dramatically slower. Additionally, certain WebGL features like real-time shadows or complex shader effects may not be hardware-accelerated on all devices.
7. Excessive draw calls. Each time the renderer switches textures, layers, blend modes, or effects, it issues a new draw call. A layout with 10 layers, each containing objects with different textures and blend modes, may generate hundreds of draw calls per frame. The Construct 3 debugger reports the draw call count — anything above 100 per frame warrants investigation.
The Fix
Step 1: Profile with the debugger. Run your project in preview mode, then open the debugger (the bug icon in the toolbar, or press the debugger shortcut). Check the Performance section for CPU time, GPU time, object count, and draw calls.
// Enable the performance display in-game for testing
Condition: System > On start of layout
Action: Text > Set text to ""
Condition: System > Every tick
Action: DebugText > Set text to
"FPS: " & round(fps) &
" | Objects: " & objectcount &
" | CPU: " & round(cpuutilisation * 100) & "%"
Step 2: Reduce particle counts. Lower the emission rate, reduce particle lifetime, and destroy emitters when they move off-screen:
// Limit particle emitters
// In the Particles object properties:
// Rate: 20 (instead of 100)
// Lifetime: 1.5 (instead of 5)
// Max particles: 50
// Destroy off-screen particle emitters
Condition: Particles > Is outside layout
Action: Particles > Destroy
// Or disable emission when off-screen
Condition: System > Every tick
Sub-event:
Condition: Particles > Is on-screen
Action: Particles > Set rate to 20
Else:
Action: Particles > Set rate to 0
Step 3: Optimize sprite sheets. Keep sprite sheet dimensions at power-of-2 sizes. Remove unused animation frames. Use the “Strip unused images” feature when exporting.
// Recommended sprite sheet sizes:
// Small sprites: 256x256 or 512x512
// Character animations: 1024x1024
// Large tilesets: 2048x2048
// Maximum recommended: 2048x2048 (mobile) / 4096x4096 (desktop)
//
// In Project Properties:
// Downscaling quality: "Medium" for mobile builds
// Image formats: Use WebP where supported for smaller file sizes
Step 4: Optimize collision checks. Replace broad “every tick” collision conditions with triggered or spatial approaches:
// SLOW: checks all bullets against all enemies every tick
Condition: Bullet > Is overlapping Enemy
Action: Enemy > Subtract 1 from health
Action: Bullet > Destroy
// FASTER: use the built-in "On collision" trigger
Condition: Bullet > On collision with Enemy
Action: Enemy > Subtract 1 from health
Action: Bullet > Destroy
// The "On collision" trigger uses spatial hashing internally
// and only fires once per collision pair, instead of
// testing every combination every tick.
Step 5: Manage object counts. Destroy objects that leave the screen or are no longer needed:
// Destroy bullets that leave the layout
Condition: Bullet > Is outside layout
Action: Bullet > Destroy
// Limit total object count with object pooling
// Instead of creating/destroying, recycle objects:
Condition: Bullet > Is outside layout
Action: Bullet > Set position to (Player.X, Player.Y)
Action: Bullet > Set angle of motion to Player.Angle
Action: Bullet > Set enabled > Enabled
// Monitor object count during development
Condition: System > objectcount > 500
Action: Browser > Log "WARNING: Object count exceeded 500"
Step 6: Reduce draw calls. Consolidate layers, minimize blend mode usage, and group same-texture objects on the same layer:
// Draw call optimization tips:
// 1. Use fewer layers (each layer = potential draw call break)
// 2. Use Tiled Background instead of many Sprite instances
// for repeated elements like ground tiles
// 3. Keep blend modes as "Normal" unless truly needed
// 4. Avoid per-object effects; use layer effects instead
// 5. Pack related images into the same sprite sheet
// so the renderer can batch them in one draw call
//
// Target: under 80 draw calls per frame on mobile
// Check: Debugger > Performance > GPU draw calls
Step 7: Handle GPU blacklisting. If performance is terrible on specific devices, it may be a WebGL issue. Add a fallback or warning:
// Detect low-performance devices at startup
Condition: System > On start of layout
Condition: System > fps < 30
Action: System > Set layer "Effects" visible > Invisible
Action: System > Set object "Particles" rate to 0
// In JavaScript (advanced): check WebGL renderer info
// System > Run JavaScript:
const gl = document.createElement("canvas").getContext("webgl");
const info = gl.getExtension("WEBGL_debug_renderer_info");
if (info) {
const renderer = gl.getParameter(info.UNMASKED_RENDERER_WEBGL);
console.log("GPU:", renderer);
// Check for known slow renderers
if (renderer.includes("SwiftShader")) {
console.warn("Software rendering detected - expect poor performance");
}
}
Why This Works
Each optimization targets a different part of the rendering and simulation pipeline:
Reducing particles directly lowers the number of GPU draw operations per frame. Each particle is a textured quad with alpha blending, which is one of the most expensive per-pixel operations. Cutting particles from 500 to 50 can recover 10-15 FPS on mobile devices.
Power-of-2 textures allow the GPU to use mipmapping efficiently and avoid NPOT texture padding. This reduces VRAM usage and improves texture cache hit rates, especially on mobile GPUs with limited cache sizes.
Triggered collisions use Construct 3’s internal spatial hash grid, which only tests objects that are near each other. The “On collision” trigger has O(n) complexity instead of the O(n²) complexity of brute-force overlap testing every tick.
Object cleanup prevents the engine from iterating over dead objects during event processing. Even if an object is off-screen, it still participates in event sheet evaluation unless destroyed. Keeping the object count under control ensures linear event processing time.
Draw call reduction minimizes the number of state changes the GPU must process per frame. Each draw call involves setting up shader uniforms, binding textures, and flushing the rendering pipeline. Fewer draw calls mean the GPU spends more time actually drawing pixels and less time waiting for state changes.
"Profile before you optimize. I have seen developers spend hours optimizing collision code when the real problem was an invisible Particles emitter spraying 1,000 particles per second behind the background layer."
Related Issues
If your game runs smoothly but the export is slow to load, the issue is asset size rather than runtime performance — check exported game not running or blank screen. If performance is fine but input feels laggy on mobile, see touch input not working on mobile. And if objects disappear when you try to optimize by moving them off-screen, check sprite not showing on layer for visibility issues.
Profile first. Optimize second. Always in that order.