Skip to content

fix: memoize per-drag derived values to keep drag.dragover cheap#2555

Draft
christianhg wants to merge 1 commit into
mainfrom
fix/dnd-dragover-perf
Draft

fix: memoize per-drag derived values to keep drag.dragover cheap#2555
christianhg wants to merge 1 commit into
mainfrom
fix/dnd-dragover-perf

Conversation

@christianhg
Copy link
Copy Markdown
Member

The drop-indicator and deserialization.success guards both compute getDragSelection, getSelectedBlocks, and isSelectingEntireBlocks on every drag.dragover event. These derive from dragOrigin.selection which is stable for the entire drag, but the recomputation is O(N) over the dragged range every event.

A single drag with "select all" on a 1000-block document was spending ~10ms per dragover (~600+ events per drag). Most of that cost is in getSelectedBlocks walking the keyed range and isSelectingEntireBlocks recomputing block boundaries — none of which can change while the drag is in flight.

Memoize the derived values on a WeakMap keyed on dragOrigin. The first event populates the cache, subsequent events do an O(1) lookup. getFocusBlock for the drop position is the only per-event tree walk that's actually necessary.

The dragOrigin object reference is stable for the entire drag (assigned once in internalDrag at drag.dragstart, returned by reference on every getSnapshot() call), so the WeakMap entry is reachable until the drag ends and internalDrag is cleared.

Behavior is unchanged. Cross-browser dnd tests green on chromium, firefox, and webkit.

Measurements

1000-block document, 100 dragover events per scenario:

Scenario Before After (estimated)
Collapsed-cursor drag 1.1ms / dragover ~0.5ms
Select-all drag 10.2ms / dragover ~0.5ms

The remaining cost per dragover is getFocusBlock for the drop position which depends on the drop position changing every event.

The drop-indicator and `deserialization.success` guards both compute
`getDragSelection`, `getSelectedBlocks`, and `isSelectingEntireBlocks`
on every `drag.dragover` event. These derive from `dragOrigin.selection`
which is stable for the entire drag, but the recomputation is O(N) over
the dragged range every event.

A single drag with "select all" on a 1000-block document was spending
~10ms per dragover (~600+ events per drag). Memoize the derived values
on a `WeakMap` keyed on `dragOrigin`. The first event populates the
cache, subsequent events do an O(1) lookup. `getFocusBlock` for the drop
position is the only per-event tree walk.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
portable-text-editor-documentation Ready Ready Preview, Comment Apr 28, 2026 11:31am
portable-text-example-basic Ready Ready Preview, Comment Apr 28, 2026 11:31am
portable-text-playground Ready Ready Preview, Comment Apr 28, 2026 11:31am

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 28, 2026

🦋 Changeset detected

Latest commit: 45655c7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@portabletext/editor Patch
@portabletext/plugin-character-pair-decorator Patch
@portabletext/plugin-emoji-picker Patch
@portabletext/plugin-input-rule Patch
@portabletext/plugin-markdown-shortcuts Patch
@portabletext/plugin-one-line Patch
@portabletext/plugin-paste-link Patch
@portabletext/plugin-sdk-value Patch
@portabletext/plugin-typeahead-picker Patch
@portabletext/plugin-typography Patch
@portabletext/toolbar Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

📦 Bundle Stats — @portabletext/editor

Compared against main (c2ec808b)

@portabletext/editor

Metric Value vs main (c2ec808)
Internal (raw) 733.2 KB -144 B, -0.0%
Internal (gzip) 140.0 KB +67 B, +0.0%
Bundled (raw) 1.33 MB -147 B, -0.0%
Bundled (gzip) 300.2 KB +63 B, +0.0%
Import time 94ms -0ms, -0.4%

@portabletext/editor/behaviors

Metric Value vs main (c2ec808)
Internal (raw) 467 B -
Internal (gzip) 207 B -
Bundled (raw) 424 B -
Bundled (gzip) 171 B -
Import time 2ms -0ms, -0.4%

@portabletext/editor/plugins

Metric Value vs main (c2ec808)
Internal (raw) 3.1 KB -
Internal (gzip) 967 B -
Bundled (raw) 2.9 KB -
Bundled (gzip) 899 B -
Import time 8ms -0ms, -1.0%

@portabletext/editor/selectors

Metric Value vs main (c2ec808)
Internal (raw) 76.5 KB -
Internal (gzip) 13.7 KB -
Bundled (raw) 71.9 KB -
Bundled (gzip) 12.6 KB -
Import time 7ms -0ms, -0.6%

@portabletext/editor/utils

Metric Value vs main (c2ec808)
Internal (raw) 27.8 KB -
Internal (gzip) 5.5 KB -
Bundled (raw) 25.4 KB -
Bundled (gzip) 5.1 KB -
Import time 6ms -0ms, -0.3%

🗺️ . · ./behaviors · ./plugins · ./selectors · ./utils · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

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