Phone performance for a plain-JS canvas game: limits, speed, workers, storage and measuring

Notes compiled 2026-11-05 for Elsewhere and the Tycho study. Target: a plain-JavaScript pixel game at 320×180, painted into palette-indexed buffers and drawn with canvas 2D (Tycho’s GPU stand-in), with seeded world generation on the order of the study’s demos (a 256×256 or 512×512 world, quest-sized regions). The question: what do real phones allow, how fast are they against the laptop the study’s numbers were measured on, and how should generation, saving and measuring be budgeted. Sibling notes with phone-relevant numbers: realistic_world_generation.md and world_generation_bench.js (laptop timings of the generation stages), falling_sand_materials.md (section 4, sand on phones), lod_zoom_chunks.md (Merryfolk’s per-frame painting budget and chunk memory).

Note on sourcing: WebKit’s blog posts, WebKit Bugzilla comments by WebKit engineers, MDN and Chrome’s developer docs were fetched and read. Phone benchmark scores come from Notebookcheck’s reviews (fetched; the tables were read through a summarizer, so treat the last digit with care). The laptop’s JetStream score and every workload time marked “measured” were measured for this note on the study’s reference machine. Items marked “(search summary)” come from search-result summaries only.

The reference machine. A 2019 MacBook Pro 16-inch: Intel Core i9-9880H at 2.3 GHz (8 cores, 16 threads), 32 GB, macOS 15.7.9, Chrome 157 headless. sysctl -n machdep.cpu.brand_string gives “Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz”. JetStream 2.2 on it scored 213 (measured, one run, browserbench.org/JetStream2.2, headless Chrome 157.0.8103.38). This is the same machine as the geography page’s bench (Node v26.8.1 there).

1. Hard limits: canvas, memory, GPU APIs

Takeaway

A 320×180 game is nowhere near any canvas limit. What bites on iOS is total memory: Safari keeps canvases alive after a page drops them, a WebContent process can be killed by jetsam past a soft limit near 1.5 GB (lower on phones with less RAM), and a killed tab reloads with “A problem repeatedly occurred”. Big prebaked chunk caches (Merryfolk’s 117 MB of canvases) are the realistic risk, not the frame. WebGL2 is everywhere since Safari 15 (2021), OffscreenCanvas 2D in workers since Safari 16.4 (2023), WebGL in OffscreenCanvas since Safari 17 (2023), WebGPU since Safari 26 (2025).

Cited findings

Inferences

2. CPU: how fast phones run JS

Takeaway

On browser benchmarks the iPhone 15 and newer beat the 2019 laptop; mid-range Android runs about a third as fast, and budget Android (Helio G81, Galaxy A15) about a quarter. Budget for the slow end: a job that takes 100 ms on the laptop takes about 400 ms on a Moto G15, and about 85 ms on an iPhone 15. Phones also throttle: the Pixel 6a loses more than half its GPU throughput in 3DMark’s 20-run stress loop; the iPhone 15 keeps 76 to 84%.

Cited findings (Notebookcheck reviews; JetStream 2 total, Speedometer 2.0 runs/min; higher is faster)

Device (browser) JetStream 2 Speedometer 2.0 Speedometer 3.0 Source
Reference: 2019 MacBook Pro, i9-9880H (headless Chrome 157) 213 (JetStream 2.2, measured)     this note
iPhone 17 Pro, A19 Pro (Safari 26.0) 329.1 621 41.7 NBC iPhone 17 Pro
iPhone 16 Pro (Safari 18) 386.7 569 33.5 same
iPhone 15 Pro (Chrome 117, WebKit) 351.1 396   NBC iPhone 15
iPhone 15, A16 (Chrome 117, WebKit) 259.7 327   same
iPhone 14 (Safari 16) 189.9 341   same
iPhone SE 2022, A15 (Chrome 99, WebKit) 182.6 239   NBC Pixel 6a
Galaxy S25 Ultra (Chrome 132) 315.1 425 29.3 NBC iPhone 17 Pro
Pixel 10 Pro (Chrome 140) 193.3 345 20.6 same
Galaxy S23 (Chrome 109) 158.6 136   NBC iPhone 15
Pixel 6 (Chrome 96) 90.1 104   NBC Pixel 6a
Pixel 6a, Tensor (listed as Firefox for JetStream; Chrome 105) 75.3 90.4   same
Galaxy A53 (Chrome 101) 63.5 51.2   same
Galaxy A15 (Chrome 122)   60.4 3.66 NBC Moto G15
Moto G15, Helio G81 (Chrome 134) 56.4 49.3 3.06 same
Redmi A3 (Chrome 126)   15.2 1.03 same
Smartphone class average (Notebookcheck) 153.3 224 14.3 NBC pages

Inferences

3. Workers: moving generation off the main thread

Takeaway

Workers are universal (module workers since Safari 15). A worker made from a Blob URL needs no extra file. Startup is tens of milliseconds on phones; transferring an ArrayBuffer is near free at any size; structured clone of ~100 KB stays inside a frame even on slow phones. SharedArrayBuffer needs cross-origin isolation (COOP/COEP headers), which GitHub Pages can’t send; a service worker (coi-serviceworker) can fake the headers at the cost of a reload on first visit. For generation, transfer (not share) is enough. Chunking work across frames with a generator and a ms budget is the no-worker fallback (Merryfolk does it).

Cited findings

Measured (this note, laptop, headless Chrome 157)

4. Storage: keeping saves and generated worlds

Takeaway

A seed plus edits is the save; generated chunks are a cache. localStorage holds about 5 MiB and is synchronous; IndexedDB holds gigabytes (up to 60% of disk per origin in Safari 17+ and Chrome) but is best-effort and evicted least-recently-used. Safari deletes all script-written storage of a site after seven days of Safari use without a user interaction with it; Home Screen web apps keep their own count and aren’t expected to lose data. navigator.storage.persist() is granted silently by heuristics in Safari (being a Home Screen app) and Chrome (engagement). CompressionStream (Safari 16.4+, Chrome 80+) compresses a chunk in a few ms.

Cited findings

Inferences

5. Measuring on the device

Cited findings

Inferences

Gaps