Back to articles
games August 2, 2026 5 min read

How I Got My Phaser.js Game to Feel Native on Mobile Browsers

I share the hard‑won tricks that let a 2D Phaser.js game run smoothly on any phone, from viewport scaling to touch controls and FPS boosts.

I started this project because every time I tried a web‑based 2D game on my phone, the canvas would either be cut off, the controls were unresponsive, or the frame rate dropped dramatically. The core problem was simple: mobile browsers treat the viewport very differently from desktop, and Phaser’s default settings don’t account for the quirks of touch input and limited GPU bandwidth. I decided to build a tiny prototype from scratch, document every dead‑end, and end up with a game that feels as responsive as a native app.

Canvas Scaling and Mobile Viewports

The first thing I noticed was that the game stretched or added black bars on devices with unusual aspect ratios. Phaser ships with a flexible scaling manager, but I had to configure it explicitly. I settled on Phaser.Scale.FIT because it preserves the aspect ratio while filling as much of the screen as possible. The key lines look like this:

I also added a listener for the resize event. Mobile browsers sometimes fire a resize when the address bar hides, and without handling it the canvas would momentarily shrink. The listener forces Phaser to recalculate the scale and keeps the game full‑screen.

Touch Input and Controls

My initial attempt was to reuse the keyboard listeners I had written for the desktop version. Unsurprisingly, nothing happened on a touch screen. I switched to Phaser’s built‑in pointer system, which abstracts mouse and touch events under a single API. The approach I took was:

The biggest surprise was the “ghost tap” bug: when a finger slid from one zone to another without lifting, Phaser would fire a pointerup on the first zone and a pointerdown on the second, causing a brief pause in movement. I solved it by checking the pointer.move event and only toggling flags when the pointer actually left the screen bounds of a zone.

Keeping FPS High on Mobile

Even after the canvas filled the screen and the controls worked, the frame rate still dipped to 20‑30 fps on mid‑range Android phones. I profiled the game with Chrome’s performance tools and identified two main culprits: overdraw from off‑screen sprites and texture swaps caused by loading many separate image files.

With these tweaks the game consistently hit 60 fps on a Pixel 4a and stayed above 45 fps on older devices.

Code Walkthrough

Below is the minimal Phaser configuration I used for the prototype. It includes the scaling settings, a simple scene that loads an atlas, creates a player sprite, and sets up the touch zones.

import Phaser from 'phaser';

const config = {
  type: Phaser.AUTO,
  parent: 'game-container',
  width: 800,
  height: 600,
  scale: {
    mode: Phaser.Scale.FIT,
    autoCenter: Phaser.Scale.CENTER_BOTH,
    orientation: Phaser.Scale.PORTRAIT,
  },
  physics: {
    default: 'arcade',
    arcade: {
      gravity: { y: 500 },
      debug: false,
    },
  },
  scene: {
    preload,
    create,
    update,
  },
};

const game = new Phaser.Game(config);

function preload() {
  this.load.atlas('sprites', 'assets/sprites.png', 'assets/sprites.json');
}

function create() {
  // Player
  this.player = this.physics.add.sprite(400, 300, 'sprites', 'player_idle');

  // Touch zones
  const leftZone = this.add.zone(0, 0, this.scale.width / 3, this.scale.height)
    .setOrigin(0)
    .setInteractive();
  const rightZone = this.add.zone(this.scale.width * 2 / 3, 0, this.scale.width / 3, this.scale.height)
    .setOrigin(0)
    .setInteractive();
  const jumpZone = this.add.zone(this.scale.width / 3, 0, this.scale.width / 3, this.scale.height / 2)
    .setOrigin(0)
    .setInteractive();

  // State flags
  this.inputState = { left: false, right: false, jump: false };

  leftZone.on('pointerdown', () => (this.inputState.left = true));
  leftZone.on('pointerup', () => (this.inputState.left = false));
  rightZone.on('pointerdown', () => (this.inputState.right = true));
  rightZone.on('pointerup', () => (this.inputState.right = false));
  jumpZone.on('pointerdown', () => (this.inputState.jump = true));
  jumpZone.on('pointerup', () => (this.inputState.jump = false));
}

function update() {
  const speed = 200;
  if (this.inputState.left) {
    this.player.setVelocityX(-speed);
    this.player.anims.play('player_walk', true);
  } else if (this.inputState.right) {
    this.player.setVelocityX(speed);
    this.player.anims.play('player_walk', true);
  } else {
    this.player.setVelocityX(0);
    this.player.anims.play('player_idle', true);
  }

  if (this.inputState.jump && this.player.body.blocked.down) {
    this.player.setVelocityY(-350);
  }
}

The code is deliberately small; the real project adds platforms, enemies, and a simple scoring system, but the scaffolding above demonstrates the three pillars I focused on: scaling, touch, and performance.

Wrap‑up

Getting a Phaser.js game to feel native on a phone took more iteration than I expected. The scaling manager saved me from writing custom CSS hacks, but I still needed to listen for browser‑specific resize events. Touch input forced me to rethink the control model entirely, and performance required a mix of culling, atlasing, and a bit of manual cache work.

Looking ahead, I plan to persist player progress using IndexedDB for offline play and eventually sync high scores to a Node.js backend via WebSockets. The next challenge will be handling real‑time multiplayer without sacrificing the frame rate we just fought hard to achieve.

Need something like this built?

I work on full-stack web apps — backend systems, APIs, and the front-ends that sit on top. If this post was useful and you've got a project that needs it, I'd like to hear about it.

Want future posts like this?

No mailing list yet — for now, email me and I'll let you know when something new goes up.

Email me