Skip to content

ScaleManager getParentBounds triggers perpetual refresh() once page is scrolled #7203

@Qualzz

Description

@Qualzz

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions