perf(frontend): virtualize grid views + the files browser (list & grid)
Extends windowing to the remaining O(n)-DOM surfaces: the card-grid view of ResourceList (recent / favorites / shared / shared-with-me / trash / search) and the main file browser (`files/[...path]`), in both list and grid layouts. - VirtualList gains a real grid mode: its inner window carries the caller's grid class (`files-grid-view`) and lays out `columns` cards per windowed row. The row pitch is auto-measured (grid card height tracks column width via the 4/3 aspect-ratio thumbnail) and re-measured on resize. - `useVirtualWindow` now distinguishes scroll from resize and exposes a `resizeTick`, so size-dependent layout (the grid pitch) is recomputed only when it can actually change. - `gridColumns(width)` (new util) mirrors the CSS `auto-fill` / `--grid-card-min` / gap so the windowed row count matches the browser's real wrapping exactly; shared by both grid callers. - The files browser flattens folders-then-files into one discriminated `entries` list rendered through VirtualList (list: columns=1; grid: columns from width). Grouped (swimlane) views stay fully rendered, as before — they're bounded. ResourceList GRID, headless Chromium (1280x900), synthetic rows, before/after: rows | mount→paint | DOM nodes | JS heap | scroll frame | jank frames ------+-------------+-----------+---------+--------------+------------ 1000 | 979→75 ms | 19k→879 | 16→3 MB | 16→17 ms | 0→0 5000 | 4005→67 ms | 95k→879 | 72→3 MB | 37→17 ms | 26→0 20000 |12015→65 ms | 380k→879 |281→7 MB |197→17 ms |1249→19 Rendered DOM and heap are flat (O(visible)) regardless of dataset size; mount is ~185x faster and scroll holds ~60fps. Verified visually mid-scroll (5 columns, 4/3 thumbnail tiles, cards land at the expected indices). The files browser shares the same VirtualList path (it can't be mounted headless — it depends on $app routing/session — so it's validated via svelte-check + the production build + the shared, separately-benchmarked component). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8Vb9QHmLZnEMzHz7MrFy6
This commit is contained in:
@@ -14,10 +14,13 @@ export class VirtualWindow {
|
||||
aboveBy = $state(0);
|
||||
/** Height of the scrollable viewport in px. */
|
||||
viewportH = $state(0);
|
||||
/** Bumped on every resize so consumers can re-measure size-dependent layout. */
|
||||
resizeTick = $state(0);
|
||||
|
||||
#root: HTMLElement | null = null;
|
||||
#scroller: HTMLElement | null = null;
|
||||
#ticking = false;
|
||||
#resizing = false;
|
||||
|
||||
/** Nearest scrollable ancestor, or null to mean the window/document. */
|
||||
#findScroller(el: HTMLElement): HTMLElement | null {
|
||||
@@ -52,20 +55,30 @@ export class VirtualWindow {
|
||||
});
|
||||
};
|
||||
|
||||
#onResize = (): void => {
|
||||
if (this.#resizing) return;
|
||||
this.#resizing = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.#resizing = false;
|
||||
this.#measure();
|
||||
this.resizeTick++;
|
||||
});
|
||||
};
|
||||
|
||||
/** Begin observing `root`; returns a teardown to call from `onMount`. */
|
||||
observe(root: HTMLElement): () => void {
|
||||
this.#root = root;
|
||||
this.#scroller = this.#findScroller(root);
|
||||
const target: EventTarget = this.#scroller ?? window;
|
||||
target.addEventListener('scroll', this.#onScroll, { passive: true });
|
||||
window.addEventListener('resize', this.#onScroll, { passive: true });
|
||||
const ro = new ResizeObserver(this.#onScroll);
|
||||
window.addEventListener('resize', this.#onResize, { passive: true });
|
||||
const ro = new ResizeObserver(this.#onResize);
|
||||
if (this.#scroller) ro.observe(this.#scroller);
|
||||
ro.observe(root);
|
||||
this.#measure();
|
||||
return () => {
|
||||
target.removeEventListener('scroll', this.#onScroll);
|
||||
window.removeEventListener('resize', this.#onScroll);
|
||||
window.removeEventListener('resize', this.#onResize);
|
||||
ro.disconnect();
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user