Skip to content

[Bug]: scrubbing the timeline fires a React state update per pointermove #407

Description

@EtienneLescot

What happens

Dragging the timeline playhead makes React log, every time:

Warning: Maximum update depth exceeded. This can happen when a component calls
setState inside useEffect, but useEffect either doesn't have a dependency array,
or one of the dependencies changes on every render.
    dispatchSetState  @ react-dom.development.js:16708
    (anonymous)       @ V4Timeline.tsx:580
    move              @ V4Timeline.tsx:628

The stack names it exactly: V4Timeline.tsx:628 is the pointermove handler of the scrub drag, and :580 is setScrubbingTimeSec(targetTime) — one React state update per mouse event, at pointer rate.

Why it is odd rather than obvious

seekToClientX already takes the performance question seriously twice over, which is what makes the third path stand out:

  • line 574 — "Direct DOM playhead update (0ms latency, zero React re-render overhead)": the playhead itself is moved by writing style.left, deliberately bypassing React;
  • line 592 — "Throttled store update / D3D seek via rAF to avoid IPC flooding": the store write and the native seek are coalesced to one per animation frame.

Between the two sits setScrubbingTimeSec, unthrottled. It looks like the one path that was not brought under the same discipline.

Why it is worth fixing rather than silencing

The warning only appears in a development build — production React strips it — so this is easy to dismiss as cosmetic. It is not quite: the update itself still happens in production, at mouse rate, on a component whose subtree re-renders during the same gesture. The warning is the symptom; the per-event state update is the cost.

It is also the same gesture that produced #395: dragging the playhead is what saturated the demuxer with seeks. That part is fixed (#400 keeps one seek in flight), and this is unrelated to the fix — but it is the other half of what a scrub costs, and it was found while investigating that bug.

Suggested direction

Coalesce setScrubbingTimeSec into the rAF that already exists a dozen lines below, so the whole gesture publishes at most one state update per frame — matching what the store write and the native seek already do. The direct DOM playhead write means nothing visible depends on it landing sooner.

Reproducing

Any project with a clip on the timeline, in a dev build: drag the playhead for a second and watch the console. Observed on main, on an instrumented build carrying no changes but logging.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions