Skip to content

fix: flatten the per-dragover cost in plugin-dnd - #3218

Merged
christianhg merged 2 commits into
mainfrom
dnd-guard-hoist
Sep 3, 2026
Merged

christianhg merged 2 commits into
mainfrom
dnd-guard-hoist

Conversation

@christianhg

@christianhg christianhg commented Sep 3, 2026 •

Copy link
Copy Markdown
Member

Two per-tick costs remain in the drag path after #3214 and #3216; this PR removes both.

The clearing behavior still treated dragenter as an end-of-interaction signal. dragenter precedes every dragover when the pointer crosses an element boundary, so each crossing ran a clear+set cycle: indicator unmount and remount, plus two caret-color writes on the editor root whose layout invalidation turns the next drag event's rect reads into forced reflows on large pages. It is the same carrier-event mistake #3214 fixed for drag.drag; dragleave still fires on every genuine departure and keeps clearing, so no stale indicator survives leaving the editor. Pinned by a test sending a dragenter between two dragovers and asserting the position and the hidden drop caret survive; red on the old guard.

The dragover guard also re-derived drag-constant facts at pointer frequency: the drag selection, the dragged block keys, the origin's focus block, and the entire-blocks check, each scanning the document. They are now derived once per (document value, drag origin) pair in the plugin closure next to the store. The value is keyed by identity; the origin by selection equality, because the origin object's identity does not survive the event pipeline, and an identity-keyed memo silently degrades to a per-event recompute (a probe counting recomputes caught exactly that). A mid-drag document change invalidates through the value key, pinned by a test where deleting a character turns the same drag origin into an entire-blocks drag and the indicator must appear; red when the memo ignores the value.

Guard decisions are unchanged; all sixteen plugin tests pass.


Note

Low Risk
Performance and event-handling tweaks in the DnD plugin with behavior pinned by existing and new tests; no auth, data, or API surface changes.

Overview
Reduces drag-and-drop overhead in @portabletext/plugin-dnd by fixing two hot-path behaviors without changing drop-indicator rules.

dragenter no longer clears the drop position. It is treated like drag.drag as a carrier event (alongside dragover), so crossing block boundaries does not run clear-then-set cycles that remount the indicator and flip the editor root’s caretColor twice per crossing—avoiding extra layout work on large pages. Clearing still runs on dragstart, dragend, dragleave, and drop.

Drag-origin facts are memoized once per drag instead of recomputed on every dragover. The dragover guard now reuses cached drag selection derivatives (dragged block keys, focus block path, entire-blocks selection) keyed by document value identity and drag-origin selection equality; cache invalidates when the document changes mid-drag (e.g. remote edit), so indicators stay correct when a partial text drag becomes a full-block drag after an edit.

New tests cover dragenter preserving position/caret and mid-drag document changes forcing recomputation.

Reviewed by Cursor Bugbot for commit ee1bb62. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Sep 3, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
portable-text-editor-documentation Ready Ready Preview Sep 3, 2026 11:28am UTC
portable-text-example-basic Ready Ready Preview Sep 3, 2026 11:28am UTC
portable-text-playground Ready Ready Preview Sep 3, 2026 11:28am UTC

Request Review

@changeset-bot

changeset-bot Bot commented Sep 3, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ee1bb62

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

This PR includes changesets to release 1 package
Name Type
@portabletext/plugin-dnd 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

github-actions Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Bundle Stats

✅ No significant changes.

All scenario measurements (7)

🗺️ @portabletext/editor / @portabletext/editor · @portabletext/editor / @portabletext/editor/behaviors · @portabletext/editor / @portabletext/editor/plugins · @portabletext/editor / @portabletext/editor/selectors · @portabletext/editor / @portabletext/editor/traversal · @portabletext/editor / @portabletext/editor/utils · @portabletext/markdown / @portabletext/markdown · Artifacts

Scenario Kind Bundle (raw / gzip) Gzip change Import time Import change
⚪ @portabletext/editor / @portabletext/editor export 1.09 MB / 253.7 KB None 70 ms -1 ms, -0.9%
⚪ @portabletext/editor / @portabletext/editor/behaviors export 4.0 KB / 1.4 KB None 2 ms -0 ms, -2.2%
⚪ @portabletext/editor / @portabletext/editor/plugins export 5.1 KB / 1.8 KB None 7 ms -0 ms, -1.1%
⚪ @portabletext/editor / @portabletext/editor/selectors export 93.7 KB / 21.3 KB None 8 ms +0 ms, +3.2%
⚪ @portabletext/editor / @portabletext/editor/traversal export 41.7 KB / 10.8 KB None 6 ms +0 ms, +0.5%
⚪ @portabletext/editor / @portabletext/editor/utils export 33.1 KB / 8.7 KB None 6 ms -0 ms, -1.8%
⚪ @portabletext/markdown / @portabletext/markdown export 272.2 KB / 79.6 KB None 41 ms -0 ms, -0.6%

Significant means at least 1.0 KB and 1% gzip, or at least 5 ms and 10% import time.

@christianhg
christianhg marked this pull request as ready for review September 3, 2026 09:43
The clearing behavior treated `drag.dragenter` as an end-of-interaction
signal, but it is a carrier event: it precedes every `dragover` when
the pointer crosses an element boundary. Each crossing therefore ran a
clear+set cycle: the indicator unmounted and remounted, and the
caret-color suppression on the editor root was restored and re-written,
two inline style writes whose layout invalidation turns the next drag
event's rect reads into forced reflows on large pages. Same mechanism
class as the `drag.drag` clearing fixed before; `dragenter` was the
remaining carrier in the guard.

Exclude `drag.dragenter` from the clearing guard. `dragleave` fires on
every genuine departure and still clears, so no stale indicator can
survive leaving the editor. Pinned by a test sending a `dragenter`
between two `dragover`s and asserting the drop position and the hidden
drop caret survive it (red on the old guard: the clear restored the
caret color synchronously).
@vercel
vercel Bot temporarily deployed to Preview – portable-text-editor-documentation September 3, 2026 11:23 Inactive
@vercel
vercel Bot temporarily deployed to Preview – portable-text-editor-documentation September 3, 2026 11:24 Inactive
@vercel
vercel Bot temporarily deployed to Preview – portable-text-editor-documentation September 3, 2026 11:26 Inactive
…gover`

The `dragover` guard re-derived drag-constant facts at pointer
frequency: `getDragSelection`, `getSelectedBlocks`, a `getFocusBlock`
resolution of the origin, and `isSelectingEntireBlocks`, each scanning
the document, so the per-event cost grew with document size.

Cache them in the plugin closure next to the drop-position store,
keyed on the document value by identity (the value array is replaced
on every applied operation) and on the drag origin by selection
equality. Identity would be the cheaper origin key, but the origin
object's identity does not survive the event pipeline, so an
identity-keyed memo silently degrades to a per-event recompute; a
probe counting recomputes caught exactly that. A mid-drag document
change (a remote edit landing) invalidates through the value key,
pinned by a test where deleting a character turns the same drag
origin into an entire-blocks drag and the indicator must appear
(red when the memo ignores the value).

The guard's observable decisions are unchanged; the self-drop
comparison now uses the cached key set and origin focus path instead
of re-deriving both.

This branch was successfully deployed

3 active deployments
Preview – portable-text-editor-documentation — ee1bb627 Deployed Sep 3, 2026 by vercel[bot]
Preview – portable-text-playground — ee1bb627 Deployed Sep 3, 2026 by vercel[bot]
Preview – portable-text-example-basic — ee1bb627 Deployed Sep 3, 2026 by vercel[bot]
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