perf: the caret's page is read after the frame, not before it
coordsAtPos called straight from the selection handler lays the whole document out before anything is painted — half a second on a 460-page file, and the writes of the load that follow it throw the result away. The read is queued as its own task and coalesced, so a run of selection changes costs one.
This commit is contained in:
1 parent
7e687b6ccb
commit
8cc6c6e20d
2 files changed
+27
-13
No files matched your search
@@ -35,6 +35,8 @@ App.svelte – owns app-level state (theme, zoom, margins, or
|
||||
└─ footer statusbar – "Page X of Y" + zoom controls (20–300%)
|
||||
```
|
||||
|
||||
`currentPage` is read one task after the selection changes (`scheduleCaretPage`): `coordsAtPos` straight from the handler lays the whole document out before anything is painted — half a second on a 460-page file, thrown away by the writes of the load that follow it.
|
||||
|
||||
`Editor.svelte` exposes `editor`, `tick`, `currentPage`, and `numPages` as bindable props to `App.svelte`, and takes `zoom`, `showFormattingMarks`, `pageMargins`, and `orientation` as inputs. `tick` is incremented on every TipTap transaction; toolbar components use `$derived(tick >= 0 && ...)` to re-evaluate `isActive`/value checks reactively without subscribing to ProseMirror directly.
|
||||
|
||||
## The settle gate (`Editor.svelte`)
|
||||
|
||||
@@ -1287,19 +1287,10 @@ import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor';
|
||||
// adding/removing rows or columns).
|
||||
scheduleTableUi();
|
||||
},
|
||||
onSelectionUpdate: ({ editor: e }) => {
|
||||
// Use the editor instance passed by TipTap directly — avoids any Svelte
|
||||
// prop-reactivity timing issues. TipTap always auto-scrolls the cursor
|
||||
// into view before firing this, so no visibility check is needed here.
|
||||
const view = paneView() ?? e.view;
|
||||
const tiptap = view.dom as HTMLElement | null;
|
||||
if (!tiptap) return;
|
||||
try {
|
||||
const coords = view.coordsAtPos(e.state.selection.head);
|
||||
const cursorInDoc = ((coords.top + coords.bottom) / 2 - tiptap.getBoundingClientRect().top) / (appliedZoom / 100);
|
||||
currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, cursorInDoc) / getCycle()) + 1));
|
||||
followCaret(currentPage);
|
||||
} catch { /* ignore */ }
|
||||
onSelectionUpdate: () => {
|
||||
// TipTap auto-scrolls the cursor into view before firing this, so the page it
|
||||
// lands on needs no visibility check of its own.
|
||||
scheduleCaretPage();
|
||||
},
|
||||
onUpdate: ({ editor: e, transaction }) => {
|
||||
saveDocument(() => e.getJSON());
|
||||
@@ -1391,6 +1382,27 @@ import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor';
|
||||
el.scrollTop = gridOrigin(el) + row * firstCycle * (appliedZoom / 100);
|
||||
}
|
||||
|
||||
// The page the caret sits on, read one task later: called straight from the selection
|
||||
// it would lay the whole document out before anything is painted — half a second on a
|
||||
// 460-page file, thrown away by the writes of the load that follow it.
|
||||
let caretPageTask: ReturnType<typeof setTimeout> | null = null;
|
||||
function scheduleCaretPage(): void {
|
||||
if (caretPageTask !== null) return;
|
||||
caretPageTask = setTimeout(() => {
|
||||
caretPageTask = null;
|
||||
if (!editor || editor.isDestroyed) return;
|
||||
const view = paneView() ?? editor.view;
|
||||
const tiptap = view.dom as HTMLElement | null;
|
||||
if (!tiptap) return;
|
||||
try {
|
||||
const coords = view.coordsAtPos(view.state.selection.head);
|
||||
const cursorInDoc = ((coords.top + coords.bottom) / 2 - tiptap.getBoundingClientRect().top) / (appliedZoom / 100);
|
||||
currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, cursorInDoc) / getCycle()) + 1));
|
||||
followCaret(currentPage);
|
||||
} catch { /* ignore */ }
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// The caret is drawn by whichever view holds the focus, so it has to be the one
|
||||
// whose cell shows the caret's page — otherwise it blinks on a clipped page.
|
||||
function followCaret(page: number) {
|
||||
|
||||
Reference in new issue
Block a user