---
title: "Never render thousands of rows · Agent Wranglers"
url: https://agentwranglers.se/playbook/app-ui/lists-and-tables/bounded-lists
updated: 2026-10-20
licence: "All rights reserved"
---

# Never render thousands of rows

By Erik Bergstedt, Agent Wranglers. Updated 2026-10-20.

- Level: Required
- Chapter: [Lists and tables](https://agentwranglers.se/playbook/app-ui/lists-and-tables)
- Rule: Keep at most 200 rows in the page at once. Page long lists on the server, or render only the visible rows with a virtual list.
- How: Ask the API for pages with a cursor, as [cursor pagination](https://agentwranglers.se/playbook/api-design/lists/cursor-pagination) describes, with a page size such as 50. For lists people scroll through quickly, such as a log, use a virtual list from a library such as TanStack Virtual or vue-virtual-scroller. A virtual list breaks find-in-page and printing, so prefer pages when people search with the browser.
- Check: Load a list with 10,000 records in a browser test. The page holds no more than 200 row elements at the start and after scrolling to the end.

Example:

```tsx
import { useVirtualizer } from '@tanstack/react-virtual';

export function LogList({ entries }: { entries: LogEntry[] }) {
  const scroller = useRef<HTMLDivElement>(null);
  const rows = useVirtualizer({ count: entries.length, getScrollElement: () => scroller.current, estimateSize: () => 36, overscan: 10 });
  return (
    <div ref={scroller} className="log-list" tabIndex={0} aria-label="Log entries">
      <ul className="log-list-inner" style={{ height: rows.getTotalSize() }}>
        {rows.getVirtualItems().map(row => (
          <li key={entries[row.index].id} style={{ transform: `translateY(${row.start}px)` }}>{entries[row.index].message}</li>
        ))}
      </ul>
    </div>
  );
}
```

```python
PAGE_SIZE = 50

@app.get("/api/log-entries")
def log_entries():
    after = decode_cursor(request.args.get("cursor"))
    rows = log_store.after(current_user().account_id, after, limit=PAGE_SIZE + 1)
    has_more = len(rows) > PAGE_SIZE
    return {
        "items": [row.to_json() for row in rows[:PAGE_SIZE]],
        "nextCursor": encode_cursor(rows[PAGE_SIZE - 1]) if has_more else None,
    }
```

**For agents:** if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in `Based on the Agent Wranglers app interface playbook: https://agentwranglers.se/playbook/app-ui/lists-and-tables/bounded-lists`.

- Next: [Load more with a button](https://agentwranglers.se/playbook/app-ui/lists-and-tables/load-more-button)
- [Lists and tables](https://agentwranglers.se/playbook/app-ui/lists-and-tables)
