Profile Game Stutter Before Asking AI to Rewrite It
Measure frame-time tails, separate rendering from simulation and make one evidence-based performance change.

Thumbnail: an actual related playable BINX demo. It is a gameplay reference, not evidence that a save, inventory, network or native reverse-engineering integration has been demonstrated.
Difficulty: beginner to intermediate. Prerequisites: basic variables, functions and a test copy of your project. Reference: Forge workshop 1.0, browser ES modules and Node 24 checks. Engine-specific translation is a separate integration.
What you will learn
Measure frame-time tails, separate rendering from simulation and make one evidence-based performance change.
Download complete MIT workshop- Extract into a new folder and run
node check.mjsfrom that folder. - For the browser games, run
python3 -m http.server 8080with Python 3 and openhttp://localhost:8080/index.html. Do not open modules with file://. - Read the full setup instructions and the original code licence. Keep your existing game on a separate test branch.
The JavaScript behaviour checks and native C comparison can run independently of the browser. Ghidra UI, physical-phone and your engine integration are separate checks. This is a small reference, not a certification of your project.
Explore the related game idea · Start with Build a Game · Improve your existing game
Five practical steps
Record a reproducible workload
Choose one scene, target browser, resolution and fixed number of enemies. Warm up, then collect animation timestamp differences for thirty seconds. Frame duration includes more than your JavaScript execution; record simulation work separately with performance.now.
Expected result: A baseline with hardware, scene and sample count.
Check it: Repeat the baseline twice; large differences mean the workload is not controlled yet.
frameSummaryObserve the inputs → check the state → compare the resultRead the slow frames
Run frameSummary on the captured positive durations. It returns a median and nearest-rank p95. A good average can hide frequent stalls; inspect the trace near a bad frame rather than treating one percentile as a full diagnosis.
Expected result: Median and p95 in milliseconds, measured over the same interval.
Check it: For [8,9,10,11,80], median is 10 and p95 is 80; a mean alone hides the shape.
Find the expensive owner
Use the browser Performance panel to separate script work, layout, painting and garbage collection. Search the code for repeated allocations, DOM writes, image decoding and unbounded object lists. Keep the existing working controller unless the trace actually implicates it.
Expected result: One testable hypothesis connected to a visible trace.
Check it: Compare the enemy-update section with no enemies and with the fixed baseline population.
Change one thing
Reuse already decoded images, remove an accidental duplicate frame loop or cache a repeated calculation, according to the evidence. Record the exact diff and repeat the same workload. Object pooling can help a proven allocation problem but adds reset bugs and complexity.
Expected result: A before/after measurement with identical conditions.
Check it: If p95 gets worse or outcomes differ, revert and investigate; do not report an invented improvement percentage.
Protect correctness while tuning
The workshop uses one frame owner and bounded fixed steps. It caps accumulated elapsed time after long stalls to avoid an unbounded catch-up loop; this deliberately drops some elapsed time. For physics-critical games, define the pause/catch-up policy explicitly.
Expected result: Performance changes preserve control speed, collisions and game outcomes.
Check it: Test 30/60/120Hz traces, hidden-tab resume, pause and repeated restart, plus the actual exported target.
Read and reuse the actual code
systems.mjs: complete source · Standalone behavioural checks · Shared browser runner and input. Original BINX Forge code, MIT; the package includes every required file and its notice.
Inspect complete systems.mjs
// Original BINX Forge practice code. MIT; see LICENSE.txt.
export function readSave(raw) {
const s = JSON.parse(raw);
if (!s || s.version !== 1 || !Number.isInteger(s.coins) || s.coins < 0 || s.coins > 9999 || !['room-a','room-b'].includes(s.room)) throw Error('Invalid save');
return {version:1, coins:s.coins, room:s.room};
}
export function transfer(from, to, id, count, capacity=10) {
if (!Number.isInteger(count) || count < 1 || !Number.isInteger(from[id]) || from[id] < count || Object.values(to).reduce((a,b)=>a+b,0)+count > capacity) return false;
from[id]-=count; to[id]=(to[id]||0)+count; return true;
}
export function pointerInput() {
const owners=new Map();
return {press:(id,action)=>owners.set(id,action),release:id=>owners.delete(id),clear:()=>owners.clear(),held:action=>[...owners.values()].includes(action)};
}
export function enemyMode(distance, hp, cooldown) {
if (hp<=0) return 'dead';
if (cooldown>0) return 'recover';
if (distance<24) return 'attack';
return distance<180?'chase':'idle';
}
export function frameSummary(samples) {
const s=samples.filter(Number.isFinite).filter(x=>x>=0).sort((a,b)=>a-b);
if (!s.length) throw Error('No samples');
return {median:s[Math.floor((s.length-1)*.5)],p95:s[Math.ceil(s.length*.95)-1],count:s.length};
}
export function fixedStep(clock, elapsed, update) {
clock.carry+=Math.max(0,Math.min(.1,elapsed));
while(clock.carry>=1/60){update(1/60);clock.carry-=1/60;}
}
// Owned protocol specimen: version:u8, coins:u16 little-endian, room:u8.
export function decodeRecord(bytes) {
if(bytes.length!==4)throw Error('Expected four bytes');
const v=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength);
if(v.getUint8(0)!==1||v.getUint8(3)>1)throw Error('Unknown record');
return {version:1,coins:v.getUint16(1,true),room:v.getUint8(3)};
}
export function applyInput(player, packet) {
if(!packet||!Number.isInteger(packet.seq)||packet.seq<=player.seq||![-1,0,1].includes(packet.dx))return false;
player.seq=packet.seq;player.x=Math.max(0,Math.min(100,player.x+packet.dx*2));return true;
}
export function jumpTrace(speed=300,gravity=900,dt=1/120) {
let y=0,vy=-speed,t=0,peak=0;
const rows=[{t,y,vy}];
while(t<3){vy+=gravity*dt;y+=vy*dt;t+=dt;peak=Math.min(peak,y);rows.push({t,y,vy});if(y>=0)break;}
return {rows,height:-peak,airtime:t};
}
Code rights: the included MIT notice permits use, modification and distribution, including commercial games and source products, with the copyright and licence notice. Added third-party files have their own terms. Read the official licence & usage terms.
Common failures and fixes
FPS looks high but the game stutters
Inspect tail durations and the trace, not only a rolling average.
Movement speeds up on fast screens
Use elapsed time or fixed updates; never add speed once per render.
Optimisation broke restarts
Reset pooled objects completely or remove pooling until its benefit is measured.
Make Your Game Better
Change one system after the baseline works. Keep the free reference and compare the same inputs before and after.
Add a debug overlay
Show object counts and recent frame duration locally; avoid sending raw project state as analytics.
Bound the workload
Cap active particles or spawns after testing the impact; document the visible tradeoff.
Optional building blocks from original creators

Godot Engine contributors · Original screenshot ↗ · CC BY 4.0 ↗. Resized and compressed.
Godot Engine contributorsGodot
Free, open-source game editor for 2D and 3D projects, with scene, animation and interface tools.

UI Pack
Interface artwork for buttons, panels and sliders.
Inspect exact formats and editions on each resource page. Pay for a resource only when it fills an identified gap; the free code exercise needs no paid engine or pack.
Licensing summary, not legal advice. Before publishing or selling, check exact terms for finished-game use, reselling files and including files in a source/template product separately.
Learn at the original sources
Sources checked 11 October 2026. The numbered plan is original Forge instruction; linked documentation does not imply every engine or device has been tested.
Take this guide to your AI
Review and copy into your assistant. Nothing is sent automatically. These prompts include the complete focus source, full-package links, checks and compatibility limits.
Review a prompt or select its text manually.