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:
Claude
2026-06-19 14:48:37 +00:00
parent ccb85f53c0
commit 8794324c3d
5 changed files with 171 additions and 86 deletions
+16
View File
@@ -0,0 +1,16 @@
/**
* Number of columns a `.files-grid-view` (and the photos square grid share the
* idea) renders at a given container width. Mirrors the CSS
* `repeat(auto-fill, minmax(var(--grid-card-min), 1fr))` so a windowing list can
* compute row counts that match the browser's actual wrapping exactly.
*
* Card-min / gap track the tokens in `lib/styles/base/variables.css` and the
* ≤640px phone override in `lib/styles/ported/resourceList.css`.
*/
export function gridColumns(width: number): number {
if (width <= 0) return 1;
const mobile = typeof window !== 'undefined' && window.matchMedia('(max-width: 640px)').matches;
const cardMin = mobile ? 140 : 200;
const gap = mobile ? 8 : 20;
return Math.max(1, Math.floor((width + gap) / (cardMin + gap)));
}