Eleven Frames Per Second on the School Laptops
For a 30-hour game jam, four Transition Year students are building a 2D space shooter in JavaScript on <canvas>. On the school laptops (dual-core i3, 4 GB RAM, integrated graphics) it runs at 11–14 fps. The starfield is drawn by looping over 60,000 pixels and calling fillRect(1, 1) for each one, every frame. Up to 40,000 particle objects accumulate in an array, and dead particles are removed with particles.splice(i, 1) from inside a for loop. Collision detection compares every bullet against every enemy. The loop is driven by setInterval(tick, 16). Chrome DevTools reports a 68 ms frame, of which 41 ms is inside drawStars. Two team members want to rewrite it in Unity; one wants to cut the star count; none of them has used Unity. The deadline is in 30 hours and the game already plays correctly.
1. Where would you start, and what evidence would you gather before changing any code?
Read the worked answer
Start from the frame budget, not from opinions. At 60 fps each frame has 16.7 ms; the current frame is 68 ms, so roughly 51 ms has to disappear. DevTools already answers where it goes: 41 ms of the 68 ms is drawStars - about 60% of the total. That single number settles the argument. Fixing it alone would bring the frame to roughly 27 ms (~37 fps), which is playable, before touching anything else.
Evidence I would add before editing:
- A profile of a representative frame - mid-game with particles at their peak, not the menu screen, since the particle array is the variable cost.
- Counts over time: how large does the particle array actually get, and how fast? 40,000 objects is a symptom worth understanding separately.
- Ruling out the trivial causes: is the canvas being resized every frame (forces a re-layout), is anything reading from the DOM in the loop, is a
console.logfiring per particle? - A baseline recording, so any change can be measured against the same scene.
On Unity: rewriting 30 hours from now, in a language and engine nobody on the team knows, with a working game as the thing being replaced, converts a performance bug into a schedule risk. Even if Unity were faster, the team could not deliver a correct game in the time. It is the wrong decision at this deadline, whatever it does to the frame rate.
2. Give three specific optimisations, and for each explain the mechanism - why it is faster, not just that it is.
Read the worked answer
1. Pre-render the starfield to an offscreen canvas once. Draw the 60,000 stars into an in-memory canvas at startup, then drawImage that canvas each frame. The mechanism: 60,000 draw calls become one blit, and each fillRect is expensive because it goes through path setup, rasterisation and state checks in the browser. The stars do not move relative to each other, so recomputing them per frame is wasted work. If the stars scroll, draw a tile twice at different offsets instead of regenerating. This attacks the 41 ms directly.
2. Stop using splice inside the loop; use swap-and-pop with a write index. splice(i, 1) shifts every later element down, which is O(n) per removal - removing many dead particles from a 40,000-element array is effectively O(n²) of array copying. Swapping the dead element with the last and decrementing the length is O(1). Even better, reuse the dead objects from a free list instead of allocating new ones, so the garbage collector stops running mid-frame and causing hitches.
3. Drive the loop with requestAnimationFrame and a delta time. setInterval(16) is not synchronised to the display: it drifts, it can queue callbacks that pile up while a frame is slow, and it keeps running in a background tab. requestAnimationFrame runs once per display refresh, pauses when hidden, and hands you a timestamp so you can move objects by elapsed time rather than assuming 16 ms. On a slow machine this prevents the feedback loop where falling behind causes more work.
Next in line, if needed: a uniform grid for collision so a bullet is only tested against nearby enemies, and a hard cap on particle spawn rate. At 60 bullets and 60 enemies the pairwise test is only 3,600 checks, so it is not the bottleneck now - measure before optimising it.
3. How would you verify the changes worked, and what would you do if the game still only reached 25 fps?
Read the worked answer
Verify against the same scene. Record a fixed scripted frame - same particle count, same number of enemies, same duration - before and after, and compare frame times, not fps. FPS hides the stutter that players actually feel: an average of 30 fps made of alternating 10 ms and 50 ms frames is worse than a steady 25. Look at the 95th percentile frame time and the longest frame. Use DevTools' performance panel with the same CPU throttling each time, or the numbers are not comparable. Also check the obvious regressions: is the starfield still there, do particles still disappear, does the game still play correctly on a different laptop model? A fast game that has silently stopped removing particles is not a fix.
If it is still 25 fps:
- Re-profile - the biggest cost has changed, and guessing again wastes the remaining hours. It is very likely now the particle update/draw or the canvas fill rate.
- Reduce fill rate before reducing logic: at 25 fps on integrated graphics, drawing fewer large shapes and avoiding alpha blending on huge areas often buys more than rewriting the update loop.
- Add adaptive degradation: measure frame time and, if it exceeds ~25 ms, halve the particle spawn rate and drop the starfield scroll. The game stays playable on the school laptops, which is the actual requirement, rather than being tuned for the developer's machine.
- Set a floor for the deadline: 25–30 fps, correct, shipped, beats an unfinished rewrite. Post-jam is the time to consider an engine change, with a working reference build to compare against.