From f477cf3cd649c4ee680f28399da42ef74e87127f Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Tue, 7 Jul 2026 00:31:36 +0200 Subject: [PATCH 01/14] [perf] Add @tanstack/vue-virtual dependency PERF-1 pilot: large productions (thousands of edits/shots/assets) freeze the page because Kitsu's datatables render every row's DOM regardless of scroll position. @tanstack/vue-virtual is a headless virtualizer that fits the intricate sticky-header/sticky-column table layout used across the list components, unlike component libraries that own their own DOM. This commit only adds the dependency; EditList.vue is virtualized as the pilot in the next commit. Co-Authored-By: Claude Fable 5 --- package-lock.json | 27 +++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 28 insertions(+) diff --git a/package-lock.json b/package-lock.json index daec580127..e4c8caa547 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@fullcalendar/vue3": "6.1.21", "@google/model-viewer": "4.3.1", "@sentry/vue": "10.63.0", + "@tanstack/vue-virtual": "^3.13.31", "@unhead/vue": "3.1.7", "@vuepic/vue-datepicker": "14.0.0", "bowser": "2.14.1", @@ -2771,6 +2772,32 @@ "dev": true, "license": "MIT" }, + "node_modules/@tanstack/virtual-core": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.3.tgz", + "integrity": "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/vue-virtual": { + "version": "3.13.31", + "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.31.tgz", + "integrity": "sha512-wZMEoSf852jQqaf3Ika1J7PiBae6341LNy/2CxmIyn0XKDQXMuK41wVX+xp6G0yx8jyR95Ef+Tdr13DK7mbJtQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.17.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.0.0" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", diff --git a/package.json b/package.json index d3f42f9f66..508efdbe9f 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@fullcalendar/vue3": "6.1.21", "@google/model-viewer": "4.3.1", "@sentry/vue": "10.63.0", + "@tanstack/vue-virtual": "^3.13.31", "@unhead/vue": "3.1.7", "@vuepic/vue-datepicker": "14.0.0", "bowser": "2.14.1", From dfcc467274183391e52630f477f9543039b1f72f Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Tue, 7 Jul 2026 00:32:02 +0200 Subject: [PATCH 02/14] [edits] Virtualize EditList rows with @tanstack/vue-virtual PERF-1 pilot on a single grid. EditList renders every edit's unconditionally, so productions with thousands of edits freeze the Edits page. useVirtualizer (added via a setup() hook, the rest of the component stays Options API) now only mounts rows near the viewport. Row-positioning technique: real / spacer rows (top/bottom) sized to the off-screen rows' total height, rather than absolutely-positioned rows with a translateY transform. Several columns in this table (metadata descriptors) have no explicit CSS width and rely on the browser's native auto-layout across all currently-rendered rows to stay aligned with the header; a transform-based tbody would force each row into its own independent `display:table` box, breaking that shared column-width resolution. Spacer rows keep every rendered row (and the header) in the same table layout context, so column widths and the existing sticky header/columns (position: sticky + the offsets computed in updateOffsets(), which only ever reads header refs) are unaffected. Row height is content-driven (big-thumbnail toggle, wrapping descriptions), so rows are dynamically measured via rowVirtualizer.measureElement, with a rough per-thumbnail-size estimate only used before a row's first measurement. `i` (a row's real index into displayedEdits, not its position among the rendered subset) is threaded through unchanged everywhere the template already depended on it (isSelected, the editor-i-j/validation-i-j ref names, the z-index stacking hack), by iterating a `visibleRows` computed of `{ edit, i }` pairs instead of `displayedEdits` directly. DOM-reads adapted: entity_list.js's onTaskSelected() rebuilds a shift-click range-selection rectangle by reading `this.$refs['validation-i-j']` for every cell in range, which silently drops any cell outside the rendered window once rows are virtualized. Overridden locally in EditList.vue (mixin methods lose to a same-named component method, so ShotList/AssetList keep the original DOM-reading version) to resolve the same rectangle from displayedEdits/taskTypeMap/taskMap instead. Known, not adapted (out of this pilot's contained scope - shared across ShotList/AssetList/EpisodeList, not just EditList): - descriptors.js keyMetadataNavigation (Ctrl+Arrow between metadata inputs) wraps first<->last row via `this.$refs['editor-i-j']`; once virtualized, wrapping to a far-away unrendered row is a silent no-op. - selection.js select()/onKeyDown (Alt+Arrow between validation cells) steps by one row via the same ref pattern; in practice low-risk since overscan=10 keeps the adjacent row mounted, but not guaranteed at the rendered window's edge. Both would need scrollToIndex + a wait-for-mount choreography that belongs with the later EntityDataGrid extraction, not this single-list pilot. No component test added: @tanstack/virtual-core's default viewport tracking needs ResizeObserver, which jsdom doesn't implement (guarded with `if (!ResizeObserver) return` in observeElementRect), and jsdom's getBoundingClientRect always returns zeros. Without either, the virtualizer's viewport rect stays stuck at its {width:0, height:0} default, so a test can't exercise "only visible rows render" - it would either pass trivially regardless of correctness or require mocking the entire size-observation pipeline. Matches this repo's existing testing split (vite.config.js: components are exercised through the dev app; coverage thresholds are scoped to src/lib and src/store only). Co-Authored-By: Claude Fable 5 --- src/components/lists/EditList.vue | 239 +++++++++++++++++++++++++++++- 1 file changed, 238 insertions(+), 1 deletion(-) diff --git a/src/components/lists/EditList.vue b/src/components/lists/EditList.vue index 469a2ee361..96025c7598 100644 --- a/src/components/lists/EditList.vue +++ b/src/components/lists/EditList.vue @@ -207,12 +207,27 @@
@@ -513,6 +534,8 @@