ScaleManager getParentBounds triggers perpetual refresh() once page is scrolled
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 40.3k
- Forks
- 7.2k
- PR merge metrics
- No merged PRs in 30d
Description
Hello, I think I might have found a bug, though I’m not entirely sure.
I’m using Phaser 3 inside a React app. The game runs smoothly at first, but as soon as I scroll the page (even by just 1px), it starts stuttering every half second.
After a lot of debugging, GPT-5-CODEX suggested a fix that solved the problem. The game now runs perfectly smoothly even after scrolling.
What’s interesting is that it claimed this was a Phaser issue. That’s where my own knowledge ends, so either it’s completely wrong and just AI nonsense, or it’s an actual bug. If it’s not a real bug, I apologize for the noise, but if it is, I figured it was worth sharing here.
I also asked it to generate a GitHub issue, which I’ve included below.
Title
ScaleManager getParentBounds triggers perpetual refresh() once page is scrolled
Body
-
Description: When running Phaser 3.90.0 with scale.mode = Phaser.Scale.FIT inside a normal scrolling page, the game begins hitching every ~500 ms after the user scrolls vertically. The
scale manager keeps calling refresh() even though layout and canvas size are unchanged, causing repeated style recalculations and visible stutter. -
Reproduction:
- Create a vanilla Phaser project (no React) with config { type: Phaser.AUTO, scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH, width: 1280, height: 720 } }.
- Mount the canvas in a tall HTML page so the document can scroll (e.g., wrap in a div with standard margins).
- Load the page in Chrome, scroll down even a few pixels, stop scrolling, and leave the game running.
- Observe frame hitches every half-second; ScaleManager.refresh() fires on each poll.
-
Expected: After scrolling stops, the scale manager should consider the layout stable and avoid invoking refresh() unless the parent element actually resizes.
-
Actual: ScaleManager.step() calls refresh() every resizeInterval (default 500 ms) indefinitely, even with no further scroll or resize events.
-
Analysis: In ScaleManager.getParentBounds() the cached canvasBounds are stored in page coordinates (rect.left + window.pageXOffset), but the subsequent comparison uses canvasRect.x / y
from getBoundingClientRect(), which are viewport-relative. Once window.pageYOffset is non-zero, canvasRect.y !== canvasBounds.y remains true forever, so refresh() runs on every poll. -
Workaround: Override getParentBounds() to normalize the coordinates or ignore pure scroll-offset changes. For example:
const original = scale.getParentBounds.bind(scale);
scale.getParentBounds = function patched() {
const changed = original();
const rect = this.canvas.getBoundingClientRect();
const scrollX = window.pageXOffset || 0;
const scrollY = window.pageYOffset || 0;
this.canvasBounds.setTo(
rect.left + scrollX - (document.documentElement.clientLeft || 0),
rect.top + scrollY - (document.documentElement.clientTop || 0),
rect.width,
rect.height
);
return changed;
};
After applying this, refresh() stops firing unless the actual size changes. -
Environment: Phaser 3.90.0, Chrome 124 / Windows 11
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Reproduce the issue with Phaser 3.90.0 using the supplied FIT-scale configuration in a scrollable page, then inspect ScaleManager.getParentBounds() and step(). Compare the cached canvasBounds coordinates with getBoundingClientRect() after scrolling, and run the reproduction until refresh() repeats; done means refresh() stops when only page scrolling has occurred while real size changes still trigger it.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- frontend, performance
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100