Skip to content

Editor input lag scales with whole-document size; _experimentalVirtualization is root-area-only, so deeply nested slot documents get no relief #1709

Description

@Daniil-Padiryakov

Description

Typing into a field gets slower as the whole document grows, not just the part you're editing. Ordinary typing already feels sluggish, and holding a key down freezes the editor for a second or two before every character lands at once.

_experimentalVirtualization would help, but it only covers the root area zone (depth === 0). If your content is wrapped in a root layout component (a common pattern), it all lives in nested slots at depth >= 1 and gets nothing.

Environment

  • Puck 0.21.3 (latest), React 18, Chrome desktop.
  • The repro runs with iframe={{ enabled: false }}. Enabling the iframe is the same or slightly worse, so the cost isn't in the iframe / style sync.

Steps to reproduce

Stock Puck, two trivial components, no resolveData / hooks.

  1. Open ?n=300&mode=nested&virt=0
  2. Select a field on the canvas
  3. Type in its label in the sidebar — laggy
  4. Hold a key down — the editor freezes

What happens

Per-keystroke time-to-paint (nested, virtualization off) climbs with node count:

nodes ~135 ~200 ~300
latency 35 ms 40 ms 54 ms

Each keystroke already runs well over one frame (16 ms), so normal typing feels janky before you ever hold a key. Holding a key at N=300 (one input per frame, ~33 ms key-repeat budget): 26 of 27 frames go over budget (worst 67 ms), so the input queue starves and the characters arrive in a burst.

Virtualization fixes this for flat / root-level content (N=300: 54 → 33 ms, 0 frames over budget) but does nothing for nested content (still ~55 ms, still freezes), because of the gate:

// components/DropZone/index.tsx
const isRootAreaZone = (areaId ?? rootAreaId) === rootAreaId && depth === 0;
const shouldVirtualizeItems = _experimentalVirtualization && isRootAreaZone;

What I expect to happen

Per-keystroke cost should depend on the field I'm editing, not the whole document, and holding a key shouldn't freeze the editor. Nested slots should get the same relief root-level content already gets.

Cause

Each keystroke dispatches a replace. The reducer skips off-path subtrees, so it's not a mass re-render of the tree — in practice only the edited leaf re-renders. But every dispatch still costs O(mounted nodes): zustand re-runs the selector of every mounted subscriber, and each node carries ~20 of them across DropZoneChild and DraggableComponent. Virtualization is the only thing that shrinks that set, and it's root-only. (Separately, a permissions subscription runs a full flattenData walk on every change even when no resolvePermissions is defined — a smaller cost on top.)

Suggested fix

useLocalValue (components/AutoField/lib/use-local-value.ts) dispatches replace synchronously on every keystroke, even though the input already renders from local state. Debouncing the store commit while the field is focused (commit on pause / blur / unmount) drops the per-keystroke dispatch entirely, regardless of document size, nesting, or virtualization.

N=300 nested, before → after: per-keystroke 54 → 33 ms; held-key 26/27 → 0/27 frames over budget. The input still renders from local state, so the caret doesn't jump and custom fields are untouched.

This is roughly the debounce the field code had in #198 and lost in the #469 refactor. Trade-off: the canvas, onAction, and resolveData update on the debounce interval instead of per character (undo history already debounces at 250 ms). Could sit behind a prop (0 = current behavior). Happy to open a PR.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions