Skip to content

Render table rows incrementally and start the data fetch earlier#663

Merged
caufieldjh merged 1 commit into
mainfrom
issue-662-render-perf
Jul 15, 2026
Merged

Render table rows incrementally and start the data fetch earlier#663
caufieldjh merged 1 commit into
mainfrom
issue-662-render-perf

Conversation

@caufieldjh

Copy link
Copy Markdown
Collaborator

Fixes #662.

After #661 removed the 30 MB download, the main page still took ~2.3 s from navigation to a rendered table. A CPU profile over the whole load showed 1151 ms idle, 799 ms "(program)", and only 98 ms of actual JS — so this was never our JS being slow. It was two structural problems.

1. The data request was blocked behind the CDN stylesheets

A parser-inserted <script> must wait for all pending stylesheets before it executes. custom.js sits at the end of <body> and runs inside jQuery(document).ready(), so the fetch couldn't start until the CDN CSS and jQuery had both resolved. The measurement was stark:

registry-data.js   downloaded at    107 ms
bootstrap.min.css  finished at     3854 ms
kgs-summary.json   fetch STARTS at 3917 ms   <-- waiting on CSS it doesn't need

The fetch needs neither jQuery nor CSS, so it now lives in a dependency-free assets/js/registry-data.js loaded from the top of <head>, above the stylesheet links, where nothing blocks it. The request now goes out at ~76–105 ms, in parallel with the CDN assets, and custom.js awaits the in-flight promise.

It's guarded to the home page (page.url == "/") so other pages don't fetch data they never use, and it derives its URLs from its own <script> src so the site's baseurl is picked up without Liquid.

2. Building 1012 rows of DOM cost ~600 ms, repaid on every filter

phase cost
build the HTML string for 1012 rows 16 ms
insert + layout all 1012 rows (~1977 KB, 8000+ cells) 597 ms
insert + layout the first 50 rows 47 ms

The cost is DOM construction, so the fix is to not build the nodes until needed. (content-visibility: auto was tried first and only gave 1.2× — it skips layout but not construction.)

The table now renders 60 rows and appends more as an IntersectionObserver sentinel scrolls into view. Because only part of the table is in the DOM, sorting can no longer reorder rows the way sorttable.js did — it now sorts the dataset and re-renders from the top, preserving the aria-sort and chevron behavior. sorttable.js has no remaining references and is removed; nothing else used it (no CSS, no other tables).

Also removed: the 50-row setTimeout chunking (it split a 16 ms string concat across ~21 macrotasks and only inserted the DOM at the very end, so it bought nothing), the MutationObserver that re-ran initSortableTables() on every mutation, and the dead $tableMain / #table-main no-ops. renderTable no longer shows a spinner on re-render, fixing the keystroke flicker noted in #662.

Results

Measured on a local Jekyll build in Chromium, navigation → first row (median of 3):

localhost 5 Mbps / 40 ms
before #661 2863 ms 49416 ms
after #661 2340 ms 3303 ms
after this PR 556 ms 2150 ms

A filter change from 1012 → 153 rows drops from ~263 ms to ~125 ms. End to end, the main page went from ~49 s to ~2.2 s on a 5 Mbps connection.

Verification

17 checks in Chromium against the built site, all passing:

  • fetch starts before jQuery lands (76 ms vs 201 ms)
  • renders a batch (60 rows), not all 1012; badge still reports the full filtered count (1012 resources)
  • scrolling appends rows and can reach all 1012; sentinel then reports "All 1012 shown"
  • sorting ascending/descending sorts the whole dataset — descending yields zwd, which DOM-only sorting of 60 loaded rows could not produce
  • license sorting works through the nested object; aria-sort state is correct
  • filters, search, and the empty-result path all still work
  • no spinner flicker while typing; no page errors

Known tradeoff

In-page Ctrl-F only finds rows that have been scrolled into existence. The search box searches the whole dataset and is unaffected. This is inherent to virtualization and was the accepted cost when choosing this approach over pagination.

Not addressed here

The code review of #661 (now merged) surfaced that its .catch(showLoadError) also wraps the render path, so an exception while rendering good data is reported to the user as a network failure. This PR touches that code but deliberately leaves the behaviour alone to keep the diff scoped — worth fixing in #661 or a follow-up.

🤖 Generated with Claude Code

After #661 removed the 30 MB download, the main page still took ~2.3s from
navigation to a rendered table. Profiling showed that was not our JS (a CPU
profile over the whole load found 1151 ms idle, 799 ms "(program)", and only
98 ms of JS) but two structural problems.

1. The data request was blocked behind the CDN stylesheets.

A parser-inserted <script> must wait for all pending stylesheets before it
executes. custom.js sits at the end of <body> and runs inside
jQuery(document).ready(), so the fetch could not start until the CDN CSS and
jQuery had both resolved. Measured: registry-data.js finished downloading at
107 ms, but the fetch did not start until 3917 ms, behind bootstrap.min.css.

The fetch needs neither jQuery nor CSS, so it now lives in a dependency-free
assets/js/registry-data.js loaded from the TOP of <head>, above the stylesheet
links, where nothing blocks it. The request now goes out at ~76-105 ms, in
parallel with the CDN assets. custom.js awaits the in-flight promise. Retry
still issues a fresh request rather than replaying the settled one.

2. Building 1012 rows of DOM cost ~600 ms, and was repaid on every filter.

Isolating the phases: building the row HTML string for 1012 rows takes 16 ms,
but inserting it (~1977 KB of HTML, 8000+ cells) takes 597 ms; the first 50
rows cost 47 ms. So the cost is DOM construction, and the fix is to not build
nodes until they are needed. (content-visibility: auto was tested first and
only gave 1.2x, since it skips layout but not construction.)

The table now renders 60 rows and appends more as an IntersectionObserver
sentinel scrolls into view. Because only part of the table is in the DOM,
sorting can no longer reorder rows the way sorttable.js did -- it now sorts the
dataset and re-renders from the top, preserving the aria-sort and chevron
behavior. sorttable.js has no remaining references and is removed; nothing else
used it (no CSS, no other tables).

Also removed as part of this: the 50-row setTimeout chunking (it split a 16 ms
string concat across ~21 macrotasks and inserted the DOM only at the end, so it
bought nothing), the MutationObserver that re-ran initSortableTables on every
mutation, and the dead $tableMain/#table-main no-ops. renderTable no longer
shows a spinner on re-render, which fixes the flicker on every keystroke noted
in #662.

Measured on a local Jekyll build, navigation -> first row (median of 3):

                       localhost      5 Mbps/40ms
  before #661           2863 ms         49416 ms
  after #661            2340 ms          3303 ms
  after this change      556 ms          2150 ms

A filter change from 1012 -> 153 rows drops from ~263 ms to ~125 ms.

Verified in Chromium against the built site: 17 checks covering the fetch
ordering, batch rendering, full-count badges, scrolling to all 1012 rows,
whole-dataset sorting in both directions (descending correctly yields "zwd",
which DOM-only sorting could not), license sorting through the nested object,
aria-sort state, filters, search, the empty-result path, and absence of spinner
flicker.

Known tradeoff: in-page Ctrl-F only finds rows that have been scrolled into
existence. The search box searches the whole dataset and is unaffected.

Refs #662

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@caufieldjh
caufieldjh merged commit 4826cfe into main Jul 15, 2026
2 checks passed
@caufieldjh
caufieldjh deleted the issue-662-render-perf branch July 15, 2026 22:28
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.

Main page still takes ~2.3s to render after data loads (fetch serialized behind CDN, 1012-row DOM build)

1 participant