Build1 distinct publisher3 min readUpdated
A developer's phone ran hot on a motionless game board. The cause was the unconditional requestAnimationFrame loop every tutorial ships, and a dirty flag fixes it without a rewrite.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A developer writing on dev.to noticed his iPhone got hot to hold while he sat on a static Ludo board in one of his own Three.js games, waiting for his turn, with nothing on screen moving [1][2]. His first instinct, which he flags as the wrong one, was that Three.js is too heavy for small games and should be replaced with plain Canvas drawing [3]. That instinct costs you a rewrite and buys nothing, because the renderer was never the variable.
Here is the loop every Three.js tutorial hands you [4]:
```js function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate(); ```
Two things are load bearing. `requestAnimationFrame` schedules a callback right before the browser paints the next frame, and browsers repaint roughly 60 times a second, so the callback runs roughly 60 times a second [5]. And `renderer.render` clears the framebuffer and redraws every object in the scene from scratch [6]. The GPU has no concept of your scene being "basically static": a motionless board at 60fps costs exactly what a board mid-animation costs, because it is doing identical work [7]. Sixty renders a second is 3,600 full-scene redraws per minute of you staring at a menu [8]; ten seconds of deciding a move is 600 of them [9].
Switching to Canvas2D does not help. According to the same post, a 60fps loop repainting a full-screen 2D canvas heats a phone just as happily [10]. The loop is the defect, not the library.
The guard is a dirty flag: a boolean you flip when something on screen actually changes, checked before you draw [11].
```js let needsRender = true; function invalidate() { needsRender = true; }
function animate() { requestAnimationFrame(animate); const moving = tweens.update() || particles.update(); if (needsRender || moving) { renderer.render(scene, camera); needsRender = false; } } animate(); ```
The rAF loop keeps ticking; only the expensive call is conditional. You call `invalidate()` from input handlers, animations and the resize listener, anywhere a pixel genuinely changes [12]. A board waiting on input then renders zero frames [13], and a turn-based game spends almost its entire life waiting [13]. When something is animating, every frame pays for a real render again, which is the behaviour you wanted all along [14]. If you are on @react-three/fiber, this is one prop: `frameloop="demand"` on the `<Canvas>`, plus `invalidate()` calls [15].
Two caveats on the evidence. The post demonstrates the difference with a side-by-side pair of simulated phones, one climbing hot and staying there, one sitting cold until tapped, and does not publish measured temperature or power numbers [16]. And the text breaks off mid-sentence while starting a second argument, that the frames you do draw each cost about twice something [17], so the per-frame half of the optimisation is not verifiable from what was published.
What to watch: grep your own projects for unconditional `renderer.render` inside a rAF callback, which is most of them if you started from a tutorial. Then check the coverage of your `invalidate()` calls, because the failure mode of render-on-demand is a frame that should have been drawn and was not: a stale board after a resize, or a hover state that never paints. That bug is visible and fixable. A hot phone in a user's hand is neither.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
With the dirty flag in place, a Ludo board waiting for input renders zero frames, and because a turn-based game spends almost its whole life doing nothing, almost all of that GPU work evaporates.
When idle, the frame falls straight through to sleep and the GPU does nothing; the moment something is moving, every frame pays for a real render again.
The post demonstrates the difference with an interactive side-by-side pair of phones, the left running the tutorial loop and climbing to hot and staying there, the right drawing only when something happened and sitting cold until tapped; the published text gives no measured temperature or power figures.
The available text of the post ends mid-sentence: "Render-on-demand kills the wasted frames. But the frames you do draw can each cost about twice wh".
The author writes on dev.to that he has several small games on his site, including Ludo, tic-tac-toe, carrom and rock-paper-scissors, built with Three.js, the standard library for drawing 3D graphics in a web page.
Testing the games on his iPhone, the author found the phone got genuinely hot to hold while he was sitting on the Ludo board waiting for his turn, with nothing on screen moving.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Mechanism shown in code, effect only anecdotal
The causal mechanism is documented directly in the source with the canonical loop, the dirty-flag replacement and the framework equivalent, which makes the reasoning inspectable. But the observed effect rests on one developer's unmeasured impression of a hot iPhone plus a qualitative two-phone demo, from a single publisher, with the text truncated mid-argument.
No adoption data beyond one hobby project
The only usage evidence is the author's own small games on his personal site. The post asserts that Three.js is the standard web 3D library and @react-three/fiber a popular wrapper, but supplies no counts, downloads, deployments or survey data on how widely the unconditional loop or the render-on-demand pattern is used, so adoption cannot be scored.
Sound advice, slightly dramatised headline
The technical claim is modest, mechanistically correct as presented, and shipped with code, which keeps the gap small. The mild overstatement is in the framing: 'cooks your phone', 'hot to hold' and 'almost all of that GPU work evaporates' are quantitative-sounding assertions backed only by an animated side-by-side demo, with no thermal or power numbers and no cross-device replication.
Developer-audience post promoting the author's own demos
This is a personal dev.to post: the incentives are reputational and audience-building, including traffic to the author's own games and interactive widgets embedded in the piece. There is no disclosed vendor sponsorship, no product being sold, and the advice points readers to free, framework-native options rather than anything the author controls.
Confident on mechanism, thin on measurement and corroboration
Single source, single publisher, no independent corroboration and no instrumentation, which caps confidence. It is raised above the midpoint because the claims are narrow, internally consistent, reproducible from the supplied code, and the article explicitly rejects the weaker framework-blame explanation rather than over-claiming.
build
Force the tool call, then hand Lightsail a long-lived key1 distinct publisher
build
AI-written code fails the same four ways, and every gate you own reports green1 distinct publisher
build
CSA's 2026 threat list is a flat line, so ask which threats a config snapshot can prove1 distinct publisher
build
An empty array is a claim about your query: verify identifiers before you trust the metric1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 16, 2026