Mục lục

Load 10.000 dòng dữ liệu: Pagination hay Infinite Scroll?

So sánh Offset-based và Cursor-based Pagination. Hướng dẫn triển khai Infinite Scroll hiệu năng cao với Intersection Observer.

Khi database của bạn có 1 triệu dòng, việc SELECT * là hành động tự sát. Bạn cần chia nhỏ data. Nhưng chia thế nào cho đúng?

1. Offset-based vs Cursor-based

Offset-based (Truyền thống)

Query: SELECT * FROM users LIMIT 10 OFFSET 1000 (Lấy trang 101).

  • Ưu điểm: Dễ code. Nhảy trang (Jump to page 50) dễ dàng.
  • Nhược điểm chết người:
    1. Chậm: Database vẫn phải quét qua 1000 dòng đầu rồi mới bỏ đi để lấy 10 dòng sau. Offset càng lớn càng chậm.
    2. Duplicate Data: Nếu User đang xem trang 1, có Admin thêm 1 user mới vào đầu bảng -> Toàn bộ danh sách bị đẩy xuống 1 dòng -> User bấm sang trang 2 sẽ thấy lại user cuối cùng của trang 1.

Cursor-based (Hiện đại - Facebook/Twitter dùng)

Query: SELECT * FROM users WHERE id < 'last_seen_id' LIMIT 10.

  • Ưu điểm:
    1. Siêu nhanh: Dùng Index của ID để nhảy cóc. Tốc độ trang 1 = trang 1000.
    2. Realtime Safe: Không bị trùng lặp khi có dữ liệu mới chèn vào.
  • Nhược điểm: Không thể "Nhảy đến trang 50". Chỉ có "Next" và "Prev".

Lời khuyên: Dùng Cursor-based cho Infinite Scroll (Feed). Dùng Offset-based cho Admin Dashboard (Table) nếu dữ liệu ít (< 100k).

2. Triển khai Infinite Scroll (Server Action + Client)

Kết hợp Server Action để fetch data và Client Component để detect scroll.

tsx:
// app/feed/actions.ts
'use server';
export async function loadMorePosts(cursor: number) {
  const posts = await db.post.findMany({
    take: 10,
    skip: 1, // Bỏ qua chính cái cursor
    cursor: { id: cursor },
    orderBy: { id: 'desc' }
  });
  return posts;
}
tsx:
// app/feed/PostList.tsx
'use client';
import { useInView } from 'react-intersection-observer';
import { useState, useEffect } from 'react';
import { loadMorePosts } from './actions';

export function PostList({ initialPosts }) {
  const [posts, setPosts] = useState(initialPosts);
  const [cursor, setCursor] = useState(initialPosts[initialPosts.length - 1]?.id);
  const { ref, inView } = useInView(); // Cái "mỏ neo" vô hình

  useEffect(() => {
    if (inView && cursor) {
      // Khi user cuộn xuống thấy cái mỏ neo
      loadMorePosts(cursor).then(newPosts => {
        setPosts([...posts, ...newPosts]);
        setCursor(newPosts[newPosts.length - 1]?.id);
      });
    }
  }, [inView]);

  return (
    <div>
      {posts.map(post => <Post key={post.id} data={post} />)}
      {/* Cái mỏ neo đặt ở cuối list */}
      <div ref={ref} className="h-10 w-full loading-spinner" /> 
    </div>
  );
}

Kết luận

Infinite Scroll tạo trải nghiệm gây nghiện (như TikTok/Facebook). Nhưng nó là cơn ác mộng về hiệu năng nếu làm sai (Memory Leak, DOM phình to). Để tối ưu hơn nữa, hãy kết hợp Infinite Scroll với List Virtualization (Chỉ render những gì User đang thấy) - xem lại bài học ở chương Performance.

Quảng cáo
mdhorizontal