Skip to content

fix(review): stop diff list re-rendering on every comment keystroke#660

Merged
backnotprop merged 2 commits intomainfrom
fix/diff-lag
May 4, 2026
Merged

fix(review): stop diff list re-rendering on every comment keystroke#660
backnotprop merged 2 commits intomainfrom
fix/diff-lag

Conversation

@backnotprop
Copy link
Copy Markdown
Owner

Closes part of #659 (fix #1: move toolbar form state out of the parent).

Summary

  • Move useAnnotationToolbar + the AnnotationToolbar / SuggestionModal JSX out of AllFilesDiffView and DiffViewer into a new ToolbarHost wrapper component.
  • Parents talk to the host through an imperative handle (forwardRef + useImperativeHandle) for handleLineSelectionEnd, handleTokenClick, and startEdit.
  • The parent's onMouseMove={toolbar.handleMouseMove} on the scroll container becomes a window-level mousemove listener inside the host (functionally equivalent — handler only stashes clientX/Y).

Why

Users reported the review UI getting laggy when typing in a comment on moderately-sized diffs. We proved with a temporary console.count probe at the top of AllFilesDiffView that the counter ticked up on every single keystroke. Cause: commentText state lived in useAnnotationToolbar which was called from AllFilesDiffView, so every keystroke re-rendered the entire all-files view → every LazyFileDiff → every Pierre <FileDiff>.

After this change, keystrokes only re-render the host. The diff list is unaffected.

Test plan

Manual verification in the dev review UI (bun run dev:review):

  • Counter probe stays flat while typing (validated locally before removing the probe).
  • Click line numbers → toolbar opens at mouse position.
  • Cmd/Ctrl+Enter submits the comment.
  • Type, click outside, reopen same range → draft restored.
  • Type, switch files → draft saved; switch back → restored.
  • "Add suggested code" → expand to modal → type → close → submit.
  • Edit existing annotation → toolbar reopens prefilled.
  • Token click → toolbar opens for single-token annotation (DiffViewer path).

Out of scope (tracked in #659)

  • Wrapping LazyFileDiff / Pierre's FileDiff in React.memo and stabilizing the inline options / renderAnnotation / renderHoverUtility props in AllFilesDiffView. Evaluated as a moderate refactor (per-file closures need a FileRow extraction); deferred since the user-reported lag is gone.
  • Adding React Compiler.
  • Server-side prerendering the review diff via preloadPatchFile (already wired up for CodeFilePopout but not the main review path).
  • Throttling capture-phase scroll listeners in AnnotationToolbar / CommentPopover / FloatingQuickLabelPicker.

…659)

Move the useAnnotationToolbar hook + AnnotationToolbar/SuggestionModal
into a new ToolbarHost wrapper so per-keystroke form-state changes no
longer re-render AllFilesDiffView / DiffViewer (and therefore not every
LazyFileDiff and Pierre <FileDiff> beneath them). Parents talk to the
host through an imperative handle for selection/edit/token-click.

Verified with a console.count probe at the top of AllFilesDiffView:
typing 20+ chars no longer increments the render counter.

Out of scope (tracked in #659): React.memo on LazyFileDiff with stable
props, React Compiler, server-side prerendering of the review diff,
throttling capture-phase scroll listeners.
backnotprop added a commit that referenced this pull request May 4, 2026
Address PR review nits on #660:

- ToolbarHost's window mousemove listener now bails on `!isFocused`,
  so dock-mounted-but-hidden panels don't track mouse for a toolbar
  the user can't see. Matches the intent of the existing isFocused
  prop already passed through to the hook for draft handling.

- Widen useAnnotationToolbar's handleMouseMove arg to a structural
  `{clientX, clientY}` shape. The hook only reads those two fields,
  so accepting the structural shape eliminates the
  `as unknown as React.MouseEvent` cast in ToolbarHost while staying
  compatible with React.MouseEvent callers.
Widen useAnnotationToolbar's handleMouseMove signature to accept the
structural { clientX, clientY } shape — the implementation only reads
those two fields, and this lets ToolbarHost pass it directly to
window.addEventListener without an `as unknown as React.MouseEvent`
cast. Function parameters are contravariant, so any caller passing a
React.MouseEvent or native MouseEvent remains valid.

No runtime change.
@backnotprop backnotprop merged commit 525c8b0 into main May 4, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant