← Recipes

阅读中文版本

Implement Infinite Scroll with IntersectionObserver

Load the next page when a sentinel enters the viewport, guard against duplicate requests, and keep a keyboard-accessible fallback for users who never scroll.

~40 minfrontendperformanceux

Task

Add cursor-based infinite scrolling that does not double-fetch and remains usable without a mouse.

When to use this

A long list renders every row at once and janks; naive infinite scroll then fires overlapping requests as the user scrolls fast.

Prerequisites

  • A paginated API with stable cursors
  • A list container with a sentinel element

Steps

1. Observe a sentinel below the list and trigger a fetch when it becomes visible.

Expected output: A new page loads only near the end of the list.

2. Track an in-flight flag and the last cursor so concurrent triggers fetch once.

curl "https://api.example.com/items?cursor=eyJpZCI6MTAwfQ"

Expected output: Rapid scrolling does not create duplicate requests.

3. Append results and stop observing when the API reports no next cursor.

Expected output: The observer disconnects at the end of the data.

4. Provide a visible Load more button as a no-JS and accessibility fallback.

Expected output: Keyboard-only users can still load pages.

Troubleshooting

Problem

Duplicate rows

Solution

Deduplicate by item id when appending; a retried request can return items already shown.

Problem

Sentinel never intersects

Solution

Ensure the sentinel has height and is not inside a display:none subtree.

Limitations

  • Infinite scroll hurts deep linking and restoring a scroll position, so keep page numbers where users need them.
  • It is unsuitable for long documents where users rely on find-in-page.

Context

Add cursor-based infinite scrolling that does not double-fetch and remains usable without a mouse. A long list renders every row at once and janks; naive infinite scroll then fires overlapping requests as the user scrolls fast.

Steps

  1. Observe a sentinel below the list and trigger a fetch when it becomes visible. Expected: A new page loads only near the end of the list.
  2. Track an in-flight flag and the last cursor so concurrent triggers fetch once. Expected: Rapid scrolling does not create duplicate requests.
  3. Append results and stop observing when the API reports no next cursor. Expected: The observer disconnects at the end of the data.
  4. Provide a visible Load more button as a no-JS and accessibility fallback. Expected: Keyboard-only users can still load pages.
const io = new IntersectionObserver(([e]) => {
  if (e.isIntersecting && !loading && cursor) loadMore(cursor);
});
io.observe(sentinel);
// always render a fallback <button>Load more</button>

Verify

  • A new page loads only near the end of the list.
  • Rapid scrolling does not create duplicate requests.
  • The observer disconnects at the end of the data.
  • Keyboard-only users can still load pages.

Limits

  • Infinite scroll hurts deep linking and restoring a scroll position, so keep page numbers where users need them.
  • It is unsuitable for long documents where users rely on find-in-page.