SfSkills Domains Docs GitHub

Home / lwc / virtualized-lists

virtualized-lists

Render only visible rows for long lists (1k+ rows) using intersection observer or lightning-datatable virtual scroll. Triggers: virtual scroll, long list LWC. NOT for hand-built windowed rendering past datatable limits — use lwc/lwc-virtualized-lists.

Open the skill folder on GitHub


LWC Virtualized Lists

Rendering 10,000 DOM nodes crashes mobile and lags desktop. This skill covers the two paths that get you to a working long list: lightning-datatable with infinite loading, and an IntersectionObserver sentinel for non-tabular content. Hand-built windowed rendering — computing a visible slice from scroll offset, spacer sizing, height measurement — is lwc/lwc-virtualized-lists.

The distinction is not stylistic. Infinite loading bounds the network work by fetching in pages; loaded rows stay in the DOM, so memory grows monotonically. Windowing bounds the DOM and is a harder build. Most Salesforce lists need only the first, plus a stated ceiling.

The Published Budget

From Improve Datatable Performance:

GuidanceValue
Best performance1,000 rows and 5 columns
Past 250 rowsfewer than 20 columns
Per requestmaximum 50 rows via LIMIT
Inline editingcritical fields only — it costs performance

Columns are a budget line, not a requirement. Row count is what teams manage; column count is what they add without noticing.

The Platform Ceiling That Shapes The Query

OFFSET cannot skip more than 2,000 records (OFFSET). Page-number pagination therefore has a hard ceiling at 2000 / pageSize, and degrades before reaching it because the database scans and discards every skipped row. Use keyset pagination — carry the last row's sort key as a cursor — with a unique tiebreak:

WHERE CreatedDate < :cursorDate
   OR (CreatedDate = :cursorDate AND Id < :cursorId)
ORDER BY CreatedDate DESC, Id DESC

Without the tiebreak, rows sharing a timestamp are dropped or duplicated at page boundaries — intermittently, which reads as a data bug.

Adoption Signals

Lists beyond 500 rows or tables beyond 1,000. Not for short lists, and not before measuring.

  1. Measure first. Chrome DevTools Performance with 4× CPU throttling. Under

    ~100 ms first render and smooth scrolling there, stop — the complexity costs a class of bugs a plain list does not have.

  2. Datatable path: enable-infinite-loading + onloadmore, inside a

    container with an explicit height — the table needs it to know where its bottom is, and the documentation ties it to preventing infinite looping. Raise load-more-offset above its 20px default so fetching starts before the user reaches blank space.

  3. Guard loadmore. It fires repeatedly while the trigger zone is visible,

    so an in-flight boolean is required — and set enableInfiniteLoading = false on an empty page, or the table asks forever at the bottom of a complete list.

  4. Custom path: one IntersectionObserver, created once (not per

    renderedCallback), with root set to the scrolling element via lwc:ref — the default root: null observes the viewport and fires continuously for a list scrolling inside its own container. disconnect() in disconnectedCallback.

  5. Filter on the server, debounced (~300 ms), and reset the cursor on every

    filter or sort change. A client-side filter over a paginated list cannot see unloaded rows — a correctness bug that presents as missing data.

  6. State a ceiling with an escape hatch. Past ~1,000 client rows, "showing

    the first 1,000 — narrow your filter or export" is a better product than a list that degrades silently.

Key Considerations

Worked Examples (see references/examples.md)

Common Gotchas (see references/gotchas.md)

Top LLM Anti-Patterns (full list in references/llm-anti-patterns.md)

Official Sources Used

Package files