Fix 50k-row edit performance: replace deep-watch undo/persistence with explicit dirty-marking
The debounced undo-history and localStorage persistence were driven by
watch(() => doc, ..., { deep: true }) -- Vue's deep watch re-traverses
the entire reactive object graph on every single mutation just to
re-establish tracking, an O(document size) cost per edit regardless of
what actually changed. Measured on a 50k-row table: one setCellInput
call went from ~450ms to <1ms with the deep watch disabled.
Both watches are replaced by scheduleSave()/markDirty(), called
explicitly by every content-mutating store function (mirroring the
existing bump()/recalcTick convention), with pure selection/navigation
functions calling scheduleSave() only so cursor movement still never
becomes an undo step. applyDocumentState (undo/redo/replaceDocument)
schedules persistence but never marks history dirty, since those
callers already reconcile history state themselves -- this also makes
the old applyingHistory guard flag dead code, now removed.
setCellInput gets an O(1) same-value no-op guard (the highest-frequency
mutator) so retyping an unchanged value creates no history noise; the
remaining no-op safety net (a real fingerprint diff) still lives in
commitHistoryStep, now running once per debounced burst instead of per
keystroke.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -148,6 +148,25 @@ wenn nichts zu tun ist); greift auch ohne Tabellenfokus, außer während eine
|
||||
Texteingabe (Zell-Editor, Formelleiste, Blatt-Umbenennen, …) den nativen
|
||||
Undo dieses Feldes benutzen soll.
|
||||
|
||||
**Performance bei großen Tabellen**: Undo-Tracking und die (debounced)
|
||||
`localStorage`-Persistenz werden **nicht** über einen `watch(doc, ...,
|
||||
{deep:true})` angestoßen — Vues Deep-Watch muss dafür bei *jeder* Mutation den
|
||||
kompletten reaktiven Objektgraphen erneut durchlaufen, ein Aufwand
|
||||
proportional zur Gesamtgröße des Dokuments statt zur eigentlichen Änderung
|
||||
(gemessen: bei einer 50.000-Zeilen-Tabelle kostete ein einzelner
|
||||
Zellen-Edit dadurch ~450 ms, unabhängig vom Umfang der Änderung). Stattdessen
|
||||
ruft jede inhaltsverändernde Store-Funktion explizit `markDirty()` auf (nach
|
||||
demselben Muster wie `bump()`/`recalcTick` für den Engine-Cache) — reine
|
||||
Auswahl-/Navigations-Funktionen rufen nur `scheduleSave()` (Persistenz ohne
|
||||
Undo-Schritt). Der Kompromiss: `markDirty()` prüft nicht mehr bei jeder
|
||||
Mutation per Fingerprint-Vergleich, ob wirklich etwas geändert wurde (genau
|
||||
dieser Vergleich bei jeder Mutation war der teure Teil) — dieses Sicherheitsnetz
|
||||
bleibt in `commitHistoryStep()` erhalten, läuft aber nur noch einmal pro
|
||||
debounced Schritt statt pro Mutation. `setCellInput` hat zusätzlich eine
|
||||
eigene, günstige No-Op-Prüfung (unverändert übernommener Wert, z. B. `F2`,
|
||||
`Enter` ohne Änderung), damit der häufigste Fall keinen Schritt erzeugt und
|
||||
den Wiederholen-Ast nicht verwirft.
|
||||
|
||||
**In-Zellen-Editor**: das Eingabefeld liegt als Overlay über der Zelle (nicht
|
||||
in ihr) und wächst mit dem Inhalt — in der Breite bis zu einer angenäherten
|
||||
„sichtbaren Anzeigebreite", danach umbrechend; in der Höhe mit `Alt`+`Enter`
|
||||
|
||||
Reference in New Issue
Block a user