Skip to content

perf(home): grid rows + content-visibility, search filters in place (metric: TBT / Style & Layout) - #7

Open
phyceClaw wants to merge 1 commit into
phyce:mainfrom
phyceClaw:perf-homepage-virtualized-rows
Open

perf(home): grid rows + content-visibility, search filters in place (metric: TBT / Style & Layout)#7
phyceClaw wants to merge 1 commit into
phyce:mainfrom
phyceClaw:perf-homepage-virtualized-rows

Conversation

@phyceClaw

@phyceClaw phyceClaw commented Jul 31, 2026

Copy link
Copy Markdown

Metric targeted: Total Blocking Time (and the Style & Layout / Rendering time behind it)

Homepage mobile scores 34. It renders all 2048 plugins — 20,590 DOM nodes — and Lighthouse mobile spends:

Style & Layout 2,690 ms
Rendering 1,646 ms
Script Evaluation 2,705 ms
Total Blocking Time 3,162 ms

Across all ten pages I audited, the mobile score tracks DOM size almost exactly (/top has 802 nodes and scores 87; /top/absolute has 24,613 and scores 49). This is the first of four PRs — one per heavy page.

Why not just add content-visibility to the <tr>

Because it does nothing. CSS containment doesn't apply to internal table elements, so the declaration is accepted and ignored. Measured in Chrome 151 over 2048 real rows at a 412 px viewport (median of 5, Chrome's own LayoutDuration + RecalcStyleDuration):

Approach Style + Layout vs today
<table> today 321.5 ms
<table> + content-visibility on <tr> 319.5 ms no change
grid rows, no content-visibility 513.2 ms worse
grid rows + content-visibility 50.3 ms −84.4%

At 1350 px: 369.7 ms → 60.4 ms (−83.7%).

So the rows have to stop being table elements. They become grid boxes carrying role="table"/"rowgroup"/"row"/"columnheader"/"cell", so the semantics survive.

The subgrid trap

Each row repeats the column track list rather than using subgrid. content-visibility implies layout containment, and containment forces a subgrid to become an independent grid — it silently loses the parent's tracks. My first version looked right in review and rendered every cell full-width, at 241 px rows instead of 41 px. An explicit per-row template is immune to this because it doesn't depend on sibling content.

I verified in Chrome that column offsets are identical between row 0 and row 900, at both 412 px and 1350 px.

Row height / scrollbar honesty

contain-intrinsic-size uses measured heights — 41 px, and 61 px below sm where the description wraps to three lines. The auto keyword means a row's true height is remembered once rendered, so the estimate only matters for rows never yet on screen. Document height comes out within ~4% of today's at desktop width.

Visible change: rows render 41 px instead of 48 px (desktop) — slightly denser. On mobile they are 61 px vs 48 px, so the page is longer there, because the fixed column tracks give the description less width than the old auto table layout did.

Search behaviour (as requested)

  • On the homepage the header search now filters the list in place, instead of opening a dropdown on top of the list it is already displaying. A match count and an empty state are shown.
  • Everywhere else it behaves exactly as before.
  • Ctrl/Cmd+F focuses the search box. Browser find only matches text that happens to be rendered; the search box matches author and tags across all 2048 plugins.
  • Sorting still applies across the whole list, and filtering preserves the chosen sort order.
  • The shared query is provided by AppLayout rather than living at module scope, which would be shared across requests under SSR.

Verification

npm run build, vue-tsc --noEmit, eslint (clean on all touched files) and the PHP suite all pass. The measurements above come from the compiled CSS of this branch rendered over the real 2048-plugin dataset in Chrome 151, not from a synthetic mock.

⚠️ What I could not do: run Lighthouse against the real app (this VPS is build-only), so the TBT figure is inferred from the Style & Layout reduction rather than measured end to end. Worth confirming on a deploy preview.

Note on conflicts

Touches Index.vue, which #5 also touches. I've aligned the colours here with #5's text-gray-400 and carried over its hidden Stats column label, so whichever lands first the other should rebase cleanly.

🤖 Generated with Claude Code

Metric: Total Blocking Time / Style & Layout (mobile home perf 34)

The homepage renders all 2048 plugins, giving 20,590 DOM nodes. Lighthouse
mobile spends 2,690 ms in Style & Layout and 1,646 ms in Rendering, and
Total Blocking Time is 3,162 ms.

content-visibility lets the browser skip style, layout and paint for rows
that are off screen. It does nothing on a <table>: containment does not
apply to internal table elements. Measured in Chrome 151 over 2048 real
rows, applying it to <tr> is not merely a no-op but slightly negative,
because Chrome pays for bookkeeping it cannot use:

  <table> today                     321.5 ms   (412px viewport)
  <table> + content-visibility      319.5 ms
  grid rows + content-visibility     50.3 ms   -84.4%

So the rows become grid boxes with ARIA table roles.

Each row repeats the column track list instead of using `subgrid`.
content-visibility implies layout containment, and containment forces a
subgrid to become an independent grid: the first attempt looked correct
in review but rendered every cell full-width, 241px rows instead of 41px.
An explicit template per row keeps columns aligned precisely because it
does not depend on sibling content. Verified against the compiled CSS
that column offsets match between row 0 and row 900.

contain-intrinsic-size uses the measured row heights (41px, and 61px
below the sm breakpoint where the description wraps to three lines) so
the scrollbar stays honest. The `auto` keyword means a row's real height
is remembered once it has been rendered.

The header search now filters the list in place on the homepage rather
than opening a dropdown over the content it is already showing;
elsewhere it is unchanged. Ctrl/Cmd+F focuses that box - browser find
only matches rendered text, while the search box matches author and tags
across every plugin. The shared query is provided by AppLayout rather
than module scope so it is per app instance under SSR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant