phaserjs / phaserjs/phaser

ScaleManager getParentBounds triggers perpetual refresh() once page is scrolled

Open
#7,203 0 comments 0 reactions 0 assignees View on GitHub

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:

    1. 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 } }.
    2. Mount the canvas in a tall HTML page so the document can scroll (e.g., wrap in a div with standard margins).
    3. Load the page in Chrome, scroll down even a few pixels, stop scrolling, and leave the game running.
    4. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.