Keep Web Game Rendering at 60 FPS on Older Phones
Smooth Cascades: Solving Match-3 Game Physics Bottlenecks
Let me tell you about a project I worked on last summer. A client came to me with a beautiful puzzle game. It had bright, colorful gems that popped, slid, and fell down the screen in big, satisfying cascades.
On my development computer, the game ran like a dream. But when we tested it on a cheap, $150 budget mobile phone, the dream turned into a nightmare. Every time a player made a match and caused a big chain reaction of falling gems, the game would stutter. The frame rate would drop from a smooth 60 frames per second down to 15. It looked awful, and players hated it.
I spent three weeks finding out why this happened. What I learned changed the way I build web games forever.
Today, I want to show you exactly why match-3 and cascading puzzle games lag on mobile browsers, and how you can use a technology called Web Workers to fix it. We will write some simple, powerful JavaScript, look at the math behind grid matching, and set up our game loops so they never drop a single frame again.
1. The Math Problem Behind Match-3 Cascades
At first glance, a match-3 puzzle game looks very simple. You just swap two items, some lines pop, and new items fall down. But under the hood, the math is actually quite heavy.
Let's look at how a browser reads a typical 8x8 game board:
- The board is represented by a grid array:
grid[row][col]. - An 8x8 grid has 64 slots.
- To check for matches, the game has to scan every row and every column.
- If it finds a match of 3, 4, or 5 matching items, it has to remove them.
- Then, it must calculate gravity: how many items fall down in each column, which empty spaces need to be filled, and where new random items should spawn.
- Once those new items land, the game has to run the entire matching scan again because those new falling items might have created new matches (a cascade).
+---------------------------------------+
| 8x8 Game Grid Array (64 elements) |
+---------------------------------------+
| [R] [G] [B] [Y] [P] [R] [G] [B] |
| [G] [R] [R] [R][Y] [P] [B] [G] <-- | Match Found!
| [B] [Y] [P] [B] [G] [R] [Y] [P] |
| [Y] [P] [G] [R] [B] [Y] [P] [B] |
| [P] [B] [Y] [P] [R] [G] [B] [Y] |
| [R] [G] [B] [Y] [P] [R] [G] [B] |
| [G] [B] [Y] [P] [R] [G] [B] [Y] |
| [B] [Y] [P] [R] [G] [B] [Y] [P] |
+---------------------------------------+
Here is where the lag comes from. Mobile browsers run almost all their tasks on a single thread called the Main Thread. This main thread has to do everything: handle user taps, calculate match gravity, run animations, and paint the pixels on the screen.
If a player triggers a massive chain reaction, your matching code has to run over and over again in a split second. If those calculations take longer than 16.6 milliseconds, the browser cannot paint the screen on time. The frame drops, the animation hitches, and your player feels the lag.
2. Moving Game Logic Off the Main Thread
To solve this problem, we need to take all the heavy math (the matching checks, the array sorting, the gravity math) and move it off the main thread entirely.
We can do this using a Web Worker.
A Web Worker is a separate JavaScript file that runs in the background, on a completely different CPU core. It cannot touch the visual page directly, but it can send messages back and forth to our main game script.
When we use Web Workers, our game loop looks like this: 1. The player swaps two items on the screen. 2. The main game script sends the grid layout to the Web Worker. 3. The Web Worker calculates the matches, calculates gravity, and builds the new grid layout in the background. 4. Meanwhile, the main thread is completely free to run smooth, uninterrupted animations at 60 FPS. 5. Once the Web Worker is done with the math, it sends the new grid data back to the main thread. 6. The main thread updates the visuals smoothly.
Using this method ensures your site stays highly responsive. In fact, if you read the Google Web Vitals guidelines, you will learn that keeping your main thread free is the single best way to maintain a low "Interaction to Next Paint" (INP) score. A low INP score keeps your users happy and helps your site rank higher in Google search results.
3. Coding the Match Finder Web Worker
Let's write a simple, practical Web Worker script. We will save this file as grid-worker.js.
This worker will receive our 8x8 grid array, search it for horizontal and vertical matches of three or more matching numbers, and return a list of coordinates that need to be popped.
// grid-worker.js - This runs on a separate background thread
// Listen for messages from the main game loop
self.onmessage = function(event) {
const { grid } = event.data;
// Find matches on the grid
const matches = findMatches(grid);
// Send the matches back to the main thread
self.postMessage({ matches });
};
/*
* Scans an 8x8 grid for matches of 3 or more matching numbers
* @param {Array<Array<number>>} grid
* @returns {Array<{row: number, col: number}>}
/
function findMatches(grid) {
const rows = grid.length;
const cols = grid[0].length;
const matchSet = new Set();
// 1. Check horizontal matches
for (let r = 0; r < rows; r++) {
let matchLength = 1;
for (let c = 0; c < cols; c++) {
if (c < cols - 1 && grid[r][c] === grid[r][c + 1] && grid[r][c] !== 0) {
matchLength++;
} else {
if (matchLength >= 3) {
// Add matching coordinates to our match set
for (let i = 0; i < matchLength; i++) {
matchSet.add(${r},${c - i});
}
}
matchLength = 1;
}
}
}
// 2. Check vertical matches
for (let c = 0; c < cols; c++) {
let matchLength = 1;
for (let r = 0; r < rows; r++) {
if (r < rows - 1 && grid[r][c] === grid[r + 1][c] && grid[r][c] !== 0) {
matchLength++;
} else {
if (matchLength >= 3) {
// Add matching coordinates to our match set
for (let i = 0; i < matchLength; i++) {
matchSet.add(${r - i},${c});
}
}
matchLength = 1;
}
}
}
// Convert our coordinates back to a clean list of objects
const result = [];
matchSet.forEach(coord => {
const [r, c] = coord.split(',').map(Number);
result.push({ row: r, col: c });
});
return result;
}
Why This Worker Script Is So Clean:
- It does not touch the document or any visual layers.
- It operates strictly on numbers. Numbers are incredibly fast for computers to process.
- By using a
Set, we automatically avoid adding the same popped item twice if a gem is part of both a horizontal and a vertical match.
4. Connecting the Worker to Your Main Game Script
Now, let's write the main thread JavaScript code. This code will load our Web Worker, set up the game board, and handle the messaging system.
// main.js - This runs on the main browser thread
// 1. Initialize the Web Worker
const gridWorker = new Worker('grid-worker.js');
// Create a dummy 8x8 grid
// 1 = Red, 2 = Green, 3 = Blue, 4 = Yellow
let gameGrid = [
[1, 2, 3, 4, 1, 2, 3, 4],
[2, 3, 4, 1, 2, 3, 4, 1],
[3, 3, 3, 2, 1, 4, 2, 3], // Look here: index 0,1,2 on this row are all '3' (Match!)
[4, 1, 2, 3, 4, 1, 2, 3],
[1, 2, 3, 4, 1, 2, 3, 4],
[2, 3, 4, 1, 2, 3, 4, 1],
[3, 4, 1, 2, 3, 4, 1, 2],
[4, 1, 2, 3, 4, 1, 2, 3]
];
// Keep track of our game state
let isCalculating = false;
// 2. Set up our worker message listener
gridWorker.onmessage = function(event) {
const { matches } = event.data;
if (matches.length > 0) {
console.log("Matches found by Web Worker:", matches);
// Trigger our visual pop animation
animatePops(matches, () =&gt; {
// Remove popped items from our grid state
removeMatchesFromGrid(matches);
// Calculate gravity and drop items down
applyGravity();
// Check again for cascades!
checkMatchesWithWorker();
});
} else {
// No more matches, player can swap again
isCalculating = false;
console.log("Grid is stable. Waiting for player's move.");
}
};
// 3. Helper function to send the grid to the worker
function checkMatchesWithWorker() {
isCalculating = true;
gridWorker.postMessage({ grid: gameGrid });
}
// 4. Dummy animation helper
function animatePops(matches, callback) {
console.log("Animating gem pops on screen...");
// Simulate a 300ms visual animation before dropping gems
setTimeout(callback, 300);
}
function removeMatchesFromGrid(matches) {
matches.forEach(m => {
gameGrid[m.row][m.col] = 0; // Set empty slots to 0
});
}
function applyGravity() {
console.log("Calculating gravity and dropping new gems...");
// In a real game, you would shift non-zero numbers down
// and fill empty slots with new random values.
}
// Start our initial scan
checkMatchesWithWorker();
By moving the search logic to grid-worker.js, our main thread only has to worry about rendering the sprites and running the CSS or Canvas animations. Even if our math functions are complex, the game animations will remain perfectly smooth at 60 FPS on any hardware.
5. Case Study: Sourcing High-Quality Puzzle Game Templates
While writing your own code from scratch is a fantastic learning experience, it can take months to design, polish, and balance a complex cascading puzzle game. Studying professional, pre-made source code is one of the fastest ways to improve your game architecture skills.
Analyzing Match-3 Code in Construct 3
If you want to study a masterfully built HTML5 match-3 project, I highly recommend looking at Gemix Puzzle - HTML5 Game | Construct 3.
This project is a premium cascading puzzle game built with the Construct 3 engine. It features stunning particle effects, rich audio tracks, smooth grid logic, and a deep level progression system.
+---------------------------------------+
| [ GEMIX PUZZLE ] |
+---------------------------------------+
| (Score: 12050) (Level: 4) |
| |
| [D] [S] [E] [E] [S] [D] [E] [D] |
| [S] [D] [D][D][D]*[S] [E] [S] |
| [E] [E] [S] [E] [D] [E] [D] [E] |
| |
| =================================== |
| * Swap gems to create chains * |
+---------------------------------------+
When you inspect a template like this, you can see how the visual rendering layers interact with the underlying grid data. It shows you exactly how to structure your game's asset loading, how to handle user touch inputs cleanly, and how to trigger cascade effects without dropping frames.
Matching vs. Casino Logic
It is also useful to compare cascading puzzle mechanics with other classic genres, such as table games. For instance, if you look at an HTML Game download of a Blackjack game, the structure is completely different.
In card games, you don't have to scan coordinate grids or calculate visual gravity. Instead, the game operates on strict mathematical logic and states (e.g., player totals, split logic, dealer thresholds). The performance focus shifts from rendering fast visual cascades to securing clean data states so that player credits and win outcomes are calculated accurately.
If you are looking to purchase clean, secure, and fully licensed codebases for your projects, I suggest visiting GPLPAL. It is my favorite white-hat repository for downloading clean HTML5 game templates, themes, and plugins under the GPL license. It is an excellent way to get professional code to study, modify, and build upon.
6. Real-World Optimization Checklist for Game Web Portals
If you run a gaming portal website or blog, keeping load times fast and frames high is crucial to your site's success. Use this straightforward checklist to audit your portal's performance:
- Implement Web Workers: For any game that uses heavy math (like complex grid checks, pathfinding, or physics simulations), move those calculations to a background thread.
- Optimize Game Assets: Compress your game's visual files. Convert heavy PNG files to modern
.webpformats, and compress your music tracks down to a lightweight.oggor.mp3format. - Control Garbage Collection: In JavaScript, creating and deleting objects inside your game loops forces the browser's "Garbage Collector" to run. This causes tiny stutters. Reuse your coordinate objects and array buffers instead of creating new ones in every loop.
- Scale Smartly: Use clean CSS layout properties (like
aspect-ratioandtransform: scale()) to scale your game viewport instead of letting the canvas redraw itself to match every screen size variation manually. - Keep Inputs Fast: Always use standard mobile touch events (
touchstartandtouchend) instead of old, slow mouse clicks to ensure there is no delay when a player taps on a gem.
By paying attention to these small details, your web games will load instantly, play smoothly on any budget smartphone, and keep your players coming back for more. Happy coding!
评论 0