# Pagination or lazy-loading handles large review sets

- **Pattern:** `ab-001208` (`ecommerce-reviews.display-ux.pagination-lazy-load`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001208
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001208)

## Why it matters

A product page that fetches all reviews without a `LIMIT` clause will transfer every row in the reviews table on every page load. At 200 reviews averaging 500 bytes each, that's 100KB of JSON for a single product visit — multiplied across concurrent users, this becomes a cost and latency problem. ISO 25010 performance-efficiency requires that resource use be proportionate to demand. More concretely, a product that accumulates 10,000 reviews will eventually time out or OOM the serverless function handling the request, causing a user-visible 500 error at scale.

## Severity rationale

Low because the performance impact only manifests at high review volumes and does not expose data or allow unauthorized actions at typical early-stage scale.

## Remediation

Add a `REVIEWS_PER_PAGE` limit to the review query and render pagination controls in `components/ReviewsList.tsx`.

```tsx
// components/ReviewsList.tsx
const REVIEWS_PER_PAGE = 10

export function ReviewsList({ productId }: { productId: string }) {
  const [page, setPage] = useState(1)
  const { data: reviews, total } = useReviews(productId, page, REVIEWS_PER_PAGE)
  const totalPages = Math.ceil(total / REVIEWS_PER_PAGE)

  return (
    <section>
      {reviews.map(r => <ReviewItem key={r.id} review={r} />)}
      {totalPages > 1 && (
        <nav aria-label="Review pages">
          {Array.from({ length: totalPages }, (_, i) => (
            <button key={i} onClick={() => setPage(i + 1)} aria-current={page === i + 1 ? 'page' : undefined}>
              {i + 1}
            </button>
          ))}
        </nav>
      )}
    </section>
  )
}
```

The corresponding API route at `api/reviews` must accept `?page=` and `?limit=` and apply `LIMIT / OFFSET` in the database query — CSS-hiding overflow is not a substitute.

## Detection

- **ID:** `pagination-lazy-load`
- **Severity:** `low`
- **What to look for:** Count the maximum number of reviews loaded per page on initial render. Check for pagination controls (page buttons, "Page X of Y"), lazy-loading (IntersectionObserver, "Load More" button), or a `limit` parameter in the review query. Report the per-page limit if found.
- **Pass criteria:** Review queries use a `limit` parameter of no more than 20 reviews per page, and the UI includes pagination controls (page numbers or "Load More" button) that fetch additional reviews on demand.
- **Fail criteria:** All reviews load at once regardless of count (no `limit` in query), or the UI fetches all reviews and only hides overflow with CSS.
- **Skip (N/A) when:** The project has no products with more than 20 reviews in the database or seed data.
- **Detail on fail:** `"Review query fetches all reviews with no LIMIT clause. A product with 200 reviews loads all 200 on initial render."`
- **Remediation:** Implement pagination in `components/ReviewsList.tsx` with a query limit:

  ```tsx
  // components/ReviewsList.tsx
  const REVIEWS_PER_PAGE = 10

  export function ReviewsList({ productId }) {
    const [page, setPage] = useState(1)
    const reviews = useReviews(productId, page, REVIEWS_PER_PAGE)
    const totalPages = Math.ceil(reviews.total / REVIEWS_PER_PAGE)

    return (
      <>
        {reviews.data.map(r => <ReviewItem key={r.id} review={r} />)}
        <div className="pagination">
          {Array.from({ length: totalPages }).map((_, i) => (
            <button key={i + 1} onClick={() => setPage(i + 1)}>
              {i + 1}
            </button>
          ))}
        </div>
      </>
    )
  }
  ```

## External references

- iso-25010 performance-efficiency

Taxons: cost-efficiency, performance

HTML version: https://auditbuffet.com/patterns/ab-001208
