The Table That Rendered Ten Thousand Rows to Show Twenty
The API returned everything and the client paginated in memory, so the DOM held ten thousand nodes, and the scroll was the layout, and the layout was the…
Start at the froze and you miss it. Start at the incident and you miss how it arrived. The bug lived in the hand-off.
I have lost on-call weeks to this exact shape of slow burn, so the order below is the order I fix it in.
Strip the client-side pagination over the full dataset down and one property makes it collapse: the visible rows are twenty and the rendered rows are ten thousand, so the layout cost is proportional to the dataset rather than the viewport.
This was an admin table in React, and the mechanics are the same for any framework that renders a list into the DOM.
Why the DOM held everything
The API returned the every row, and the every row was the convenience, and the convenience was the client's filter, and the filter was the instant, and the instant was the appeal, and the appeal was the no server round trip, and the no round trip was the decision, and the decision was the ten thousand, and the ten thousand was the rendered, and the rendered was the pagination's illusion, because the pagination hid the rows visually and not from the DOM.
The hidden rows were the display none or the slice, and the slice was the twenty, and the twenty was the visible, and the visible was the not the rendered, and the not rendered was the wrong, because the initial render built the every node, and the every node was the layout, and the layout was the freeze, and the freeze was the incident.
The DOM node is the expensive, and the expensive is the memory and the layout and the paint, and the three are the per node, and the per node is the ten thousand times, and the times is the seconds, and the seconds is the freeze, and the freeze is the reason the rendered count should equal the visible count.
Why the dataset growth broke it
The table worked at the hundred rows, and the hundred was the fast, and the fast was the confidence, and the confidence was the deploy, and the deploy was the growth, and the growth was the ten thousand, and the ten thousand was the hundred times, and the hundred times was the freeze, and the freeze was the incident, and the incident is the reason the client-side pagination is the dataset size's bet, and the bet is the loss.
The growth is the certainty, and the certainty is the eventual freeze, and the freeze is the when rather than the if, and the if is the hope, and the hope is the not the design, and the design is the server's pagination, and the pagination is the fix, and the fix is the discipline.
This is the same shape as the offset pagination that scanned every row it skipped, and the common root is the property, not the platform.
The fixes
Moved the pagination to the server. The API returned the page, and the page was the twenty rows, and the twenty was the DOM's size, and the size was the constant, and the constant was the fix, and the fix was the endpoint, and the endpoint was the discipline, because the client should render what the user sees.
Used the windowing for the long list. The virtual list rendered the viewport's rows, and the viewport was the twenty, and the twenty was the DOM's constant, and the constant was the fix, and the fix was the library, and the library was the react-window, and the window was the discipline, because the windowing keeps the DOM small while the scroll feels complete.
Removed the hidden rows from the DOM. The unmount was the removal, and the removal was the layout's cost, and the cost was the reduction, and the reduction was the fix, and the fix was the conditional render, and the conditional was the discipline, because the display none node is still laid out.
Added the search and filter to the server. The filter was the query parameter, and the parameter was the server's work, and the work was the reduced payload, and the payload was the fix, and the fix was the endpoint, and the endpoint was the discipline, because the client-side filter requires the client-side everything.
Set the row count alert on the API response. The count was the logged, and the logged was the growth's visibility, and the visibility was the alert, and the alert was the prevention, and the prevention was the fix, and the fix was the metric, and the metric was the discipline, because the unmeasured payload grows until it breaks.
If it happens again
Rendering the full dataset and paginating in the DOM makes the layout cost proportional to the data rather than the viewport, so the page freezes as the data grows. Paginate on the server, use windowing for long lists, unmount the hidden rows rather than hiding them, move the filters server-side, and alert on the response row count.
The admin table froze on scroll and the visible rows were twenty, because the API returned all ten thousand records and the client rendered every one of them into the DOM while showing a paginated slice. Name that one property in review and this class of bug gets hard to ship.
Related reading: The Screen That Made Forty Requests to Render Once explores a connected angle.
Related reading: The Stylesheet That Blocked the Render for Two Seconds explores a connected angle.
Related reading: The Four Megabyte Hero Image the CMS Never Resized explores a connected angle.
Published on debugly.dev by Rohit Bhadani | Founder of Krova Cloud