Repository navigation
perf(table): stop re-rendering every row after the table mounts - #860
Merged
Merged
Conversation
Table forwarded its slots to TableBody through a v-for, which Vue compiles as dynamic slots, so every Table render also re-rendered every row. On mount, Table re-renders for the measured head height and viewport width and for scroll edges reassigned with the same values; each pass redrew the rows. - Render TableBody from a functional outlet that tracks only its context, and declare Table's slots with Vue's Slots type, the contract the forwarding outlet inferred. - Keep the sticky-pin flags the rows read as booleans, so a viewport measurement that pins nothing differently does not reach the rows. - Store scroll edges per key, so an unchanged edge triggers nothing, on mount and on every scroll event. - Skip a table preference restore whose stored value equals the state, which replaced the column order with an identical array on mount.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On table pages, every row rendered 9 times per page load. Table forwarded its slots to TableBody through a
v-for, which Vue compiles as dynamic slots (DYNAMIC_SLOTS), and Vue'sshouldUpdateComponentalways updates a child that receives dynamic slots. So every Table render re-rendered all the rows. After mount, Table re-renders when it measures the head height and the viewport width, and whensyncScrollEdgesassigns a new edges object with the same values. Separately, the restored column order replaces the state with an identical array.Table.vue: TableBody is rendered byTableBodyOutlet = () => h(TableBody<T>, { context }, slots). Table's slots are declared withdefineSlots<Slots>(), which keeps the contract the forwarding outlet produced: any slot name, any props.tableComponentPinning.ts: the rows read the sticky-pin sides as two boolean computeds. A viewport measurement that pins nothing differently no longer reaches the rows.tableComponentScroll.ts: scroll edges are areactiveobject updated per key. An unchanged edge triggers nothing, on mount and on every scroll event.useTablePreferences.ts:restoreskips a stored value equal to the state. Table's setup write was persisted and then read back on mount as a new, identical array.Related issue
ANT-2077, found by ANT-2060's post-mount profile.
Evidence
Rows render once per real change. V8 precise-coverage call counts on the production
Tablechunk, no throttling, 3 loads per page, the same counts every time. The row count is the TableBody row callback divided by 10 rows./table-view/table-view-users/table-view/table-view-drawer(17 columns, scroll fades)The 5 renders left are the hydration render, the column order echoed back by TableView's
v-modelduring hydration, a new emptypresenceByRowfrom the presence layer, and the loading true→false refetch after mount. None of them comes from a layout measurement.Timing. Low-end profile: 4x CPU and Fast 4G (165 ms RTT, 8.1/1.35 Mbps). Production builds of
f5b1cf19and this branch on two isolateddms-controlplaygrounds, both behind the same local HTTP/2 proxy. 7 interleaved runs per cell, alternating arm order, 84 runs, 0 failed requests, every load 200 with 10 rows, load average 0.7–2.3. The metric is long-task time afterdata-dms-ready, median [range]:The main thread goes idle 94–172 ms sooner on both grids, with no change on the edit page. The playground pages show 10 rows; the cost scales with rows and cells, and the demo's grid shows 25.
Test.
tests/table-preferences-restore.test.ts: a stored column order equal to the state leaves the state's array as it is.No visual change. The wide grid at scroll start, middle and end, the narrow grid, and an expanded row were captured on both builds. Header widths, sticky offsets and fade sides are identical. The pixels differ only in the run's own seeded dates and the account e-mail. The screenshots stay outside this PR.
Verification
pnpm run checks: 11 passed, 0 failed.pnpm --dir packages/dms/frontend-vue test: 448 files, 3246 passed, 1 skipped.typecheckandlintpass (0 errors, no new warnings).main.smokeand the wholemodule-pagesfeature, 8 of 8 journeys PASS.doors: none.test:unit,test:integration) were not run: no backend file changed.Merge danger
Door: two-way. The change is client rendering only, with no stored data or contract change.
Blast radius: tables. If a slot's presence changes after mount, TableBody picks it up on its next own update rather than on Table's. Table already reads
slots.expandedonce at setup.Checklist