Why does my Phaser game stutter?
Your Phaser game runs at a smooth 60 fps, and then, every so often, it doesn't. A short freeze when enemies spawn, a hitch when a new scene starts, a stutter that only happens on someone else's laptop.
Phaser's built-in FPS counter tells you that frames dropped, but not why. Myaku tells you why: it watches every frame and explains the slow ones in plain words.
Add it to your game
npm install -D myaku// main.ts, the first line
import "myaku/auto";
import Phaser from "phaser";A small frame graph appears in the corner. Red spikes are hitches. Hover the graph to hold it still, then click a spike.
See which scene is slow
Phaser owns the game loop, so a slow frame looks, from the browser's point of view, like "Phaser's step function took 40 ms". That's true and useless. Myaku adds spans for Phaser's own loop, so the verdict says where inside it the time went:
update: all scenes' update workupdate:<scene key>: each scene on its own, e.g.update:levelorupdate:hudrender: drawing
With Phaser 3, or Phaser loaded from a script tag, this is automatic: myaku finds the global Phaser and times every game and scene. We tried it on Phaser's own Runner example, with no changes to the game, and got update, update:game and render for every frame.
Phaser 4 with ES modules has no global, so hand myaku the game:
import { adapt, myaku } from "myaku";
const game = new Phaser.Game(config);
adapt(myaku(), game);A verdict on a slow frame then reads something like:
Frame 812 took 38 ms (budget 25 ms). The game's frame code ran 35 ms. Spans: update 33 ms, update:level 31 ms, render 2.0 ms.
Now you know it's the level scene's update, not drawing. Wrap the systems inside it in your own spans to narrow it down:
const profiler = myaku();
class Level extends Phaser.Scene {
update(time: number, delta: number) {
profiler.span("enemies", () => this.updateEnemies(delta));
profiler.span("physics", () => this.physics.world.update(time, delta));
}
}The usual Phaser suspects
These are the causes myaku reports most in Phaser games, and what to do about each:
upload: textures created mid-play.this.add.text()creates a canvas texture, so creating text objects inupdate, or changing their text every frame, uploads to the GPU each time. So doesthis.textures.addCanvasor generating textures at runtime. Create them increate()and reuse them, or use bitmap text. Myaku counts the bytes: one test frame uploaded 64 MB.compile: the first use of a shader. Post-processing pipelines and FX compile their shaders the first time they're used. Warm them up during a loading scene.gc: allocation in hot paths.new Phaser.Math.Vector2()inupdate, arrayfilter/mapover all enemies every frame, closures in tweens created per frame. Reuse objects and use groups with pooling (group.get()).scriptwith a location. When a slow function runs outside Phaser's loop, in a loader callback or an input handler, Chrome tells myaku exactly which function. Myaku names it, with the file.throttled: not your game. If your window was covered or minimised, the browser slows frames down. Myaku says so instead of blaming the game.
Numbers you can share
window.__MYAKU__.report() gives the frame-time percentiles (p50, p95, p99), hitches per minute, the worst hitches with verdicts, and the average cost of each span. It's plain JSON, so you can paste it into an issue or check it from a Playwright script.
Get started with Myaku → · Spans and engines · Reading verdicts