Skip to content

fix(utils): ensure consistent throttle to next frame#6409

Merged
steveruizok merged 7 commits intomainfrom
shrink-timeout
Jul 10, 2025
Merged

fix(utils): ensure consistent throttle to next frame#6409
steveruizok merged 7 commits intomainfrom
shrink-timeout

Conversation

@steveruizok
Copy link
Copy Markdown
Collaborator

@steveruizok steveruizok commented Jul 8, 2025

This pull request refactors the throttle utility in packages/utils/src/lib/throttle.ts, used in throttleToNextFrame.

Fixes a case where the timeout could fire too often (or irregularly) due to the saving up of overflow ms.
Fixes a case where the timeout could fire too late due to a second raf.

How it works

image

Overflow

Previously, when tick recognized that 16ms or more of time had elapsed since the previous tick, it would stash any additional ms for next time. So for example a frame with a 18ms would cause the tick to run, with the "extra" 2ms stashed for next time, so that a following frame at 14ms would cause the tick to run again. This is intended to make the tick more regular, however it can also produce ticks that, being too close together, seemed to lead to make long frames harder to recover from. I think it's more important that ticks are no less than 16ms apart.

Double frames

The throttleToNextFrame helper could previously double-raf its callback.

Previously, the tick function puts its flush call behind a raf so that it occurs on the next frame. However, combined with the logic for enforcing the FPS, this meant that tick() could be delayed first by "too soon" raf, then again by the "next frame" raf, even though the "too soon" raf also would satisfy the "are we on the next frame" requirement.

Normal healthy behavior :

frame 1, 0ms = tick, raf for flush
frame 2, 8ms =  flush

Problem behavior. Because tick2 was "between ticks", it got delayed to frame 3, and then again to frame 4; from the perspective of its called, it should have run on frame 3.

frame 1, 0ms = tick1, raf for flush1
frame 2, 8ms =  flush1, tick2 (too soon), raf for tick3
frame 3, 16ms = tick3 from raf, raf for flush2
frame 4, 24ms = flush2

This is presuming that throttleToNextFrame is really about the next frame and not about the next tick, which I think is correct.

But why

I was looking at how tldraw recovered from long frames, and digging deeper into the performance tab, I was seeing ticks where I wasn't expecting them: either adjacent to other ticks (with less than 16ms between them) and then not in other places.

Change type

  • bugfix

Test plan

  • Unit tests
  • End to end tests

Release notes

  • Improved the timer to create more reliable frame updates

@huppy-bot huppy-bot bot added the bugfix Bug fix label Jul 8, 2025
@vercel
Copy link
Copy Markdown

vercel bot commented Jul 8, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
analytics ✅ Ready (Inspect) Visit Preview Jul 10, 2025 1:31pm
examples ✅ Ready (Inspect) Visit Preview Jul 10, 2025 1:31pm
tldraw-docs ✅ Ready (Inspect) Visit Preview Jul 10, 2025 1:31pm

@steveruizok steveruizok changed the title Remove throttle overflow and raf from our throttle function Consistent throttle Jul 8, 2025
Copy link
Copy Markdown
Member

@mimecuvalo mimecuvalo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels like a time where we shouldn't be maintaining/rebuilding this in-house.
why aren't we just using lodash → throttle?

@steveruizok steveruizok changed the title Consistent throttle Consistent throttle to next frame. Jul 8, 2025
@steveruizok
Copy link
Copy Markdown
Collaborator Author

this feels like a time where we shouldn't be maintaining/rebuilding this in-house. why aren't we just using lodash → throttle?

Its not really a throttle function, its about our tick / maximum FPS

let frame: number | undefined
let time = 0
let last = 0
const targetTimePerFrame = Math.floor(1000 / targetFps) // 16ms
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is better

@steveruizok
Copy link
Copy Markdown
Collaborator Author

bless me i'm landing

@steveruizok steveruizok added this pull request to the merge queue Jul 10, 2025
Merged via the queue into main with commit 9a4a83a Jul 10, 2025
14 checks passed
@steveruizok steveruizok deleted the shrink-timeout branch July 10, 2025 15:28
@mimecuvalo mimecuvalo mentioned this pull request Jul 17, 2025
3 tasks
@steveruizok steveruizok added the sdk Affects the tldraw sdk label Jan 2, 2026
@steveruizok steveruizok changed the title Consistent throttle to next frame. fix(utils): ensure consistent throttle to next frame Jan 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Bug fix sdk Affects the tldraw sdk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants