This is the third post in a series about building AniMate Waifu, a desktop companion that puts a VRM character on top of your desktop. Post one covered the three problems that fought back the hardest; post two was about why an imported model usually isn't the thing that breaks. This one is about the constraint that shaped most of the rendering code: an overlay that lives on someone's desktop all day has to be cheap enough that they never have to think about it.
A desktop pet is not a game. A game gets your full attention for an hour; a desktop companion shares your screen with everything else you do, for as long as you keep it open. The character sits in a corner while you read, write, or wait for a build. That means idle is not an edge case — idle is the default state, and idle is what has to be cheap.
Cap the loop first
The simplest decision had the biggest effect: the render loop doesn't run uncapped. The default target is 30 fps, with 15 / 30 / 60 selectable. Nothing about a character that occupies a corner of your screen needs 120 fps — above roughly 30, the extra frames are electricity, not smoothness.
Idle should cost less than active
On top of the cap, the app watches for interaction. After 30 seconds with no activity it drops to 15 fps; the next interaction restores the normal rate immediately. The exact numbers matter less than the signal: "the user hasn't touched it in a while" is information you already have for free, and acting on it is the difference between an overlay people keep running and one they quit.
One pacing bug worth passing on
The throttle itself had a subtle failure. requestAnimationFrame fires at your display's refresh rate, and the naive pattern — skip the frame if not enough time has elapsed, otherwise set lastTime = now — drifts on displays that don't divide cleanly. On a 59.94 Hz panel the fractional remainder accumulates until the throttle starts dropping every other render, and the character stutters for no visible reason. The fix is one line: carry the remainder forward (lastTime = lastTime + interval) instead of resetting it to now. Small, but it's exactly the kind of thing that shows up as "feels janky on some machines" and nowhere else.
Throttling is contextual, not global
The idle rule applies to the overlay — not to every surface in the app. The Workshop, where you preview characters, dances and stages, opts out of the throttle entirely while it is on screen, because judging a dance at 15 fps is useless. One shared frame controller, different policies per surface.
Verify it on your own machine
If you run any desktop pet — ours or someone else's — open Task Manager or Activity Monitor, leave it alone for half a minute, and watch: idle usage should be visibly lower than active usage. If a pet burns the same CPU doing nothing as it does dancing, it is rendering every frame for no reason.
And if ours is the one misbehaving on your machine, the four steps to find the real fix on our site are the user-facing version of this post — no code required.
Issues and releases live on the GitHub repo.
Next in the series
One promised topic left: the licensing side — which VRM models you can and can't show or ship, and why "it was on the internet" isn't a licence. If you've tuned an always-on app for power, I'd like to hear what worked.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.