0

Kỹ thuật Infinite Scroll tối ưu hóa hiệu năng với GraphQL

I. Giới thiệu

Infinite Scroll là gì?

Infinite Scroll (cuộn vô hạn) là một kỹ thuật giúp người dùng trải nghiệm nội dung mà không cần chuyển trang thủ công. Khi người dùng cuộn đến cuối trang, dữ liệu mới sẽ tự động được tải xuống và hiển thị liên tục. Kỹ thuật này thường thấy trên các nền tảng mạng xã hội như Facebook, Instagram hoặc các trang thương mại điện tử.

Tại sao kết hợp Infinite Scroll với GraphQL?

GraphQL cung cấp một cơ chế linh hoạt cho việc truy vấn dữ liệu, cho phép lấy đúng và đủ thông tin cần thiết. Khi kết hợp Infinite Scroll với GraphQL, chúng ta có thể:

  • Tối ưu hóa lượng dữ liệu trả về: Chỉ lấy các trường dữ liệu cần thiết, tránh dư thừa.
  • Cải thiện hiệu năng: Hỗ trợ phân trang kiểu cursor giúp giảm chi phí truy vấn.
  • Trải nghiệm mượt mà: Tải dữ liệu khi cần, giảm tải cho server và tăng tốc độ phản hồi.

II. Cơ chế hoạt động của Infinite Scroll với GraphQL

1. Cursor-based Pagination vs. Offset-based Pagination

Phân trang là cốt lõi của kỹ thuật Infinite Scroll. Có hai phương pháp chính để phân trang:

  • Offset-based Pagination: Sử dụng chỉ số (ví dụ: LIMITOFFSET trong SQL) để lấy dữ liệu.

    • Ưu điểm: Dễ triển khai.
    • Nhược điểm: Hiệu năng kém khi dữ liệu lớn, khó xử lý cập nhật động.
  • Cursor-based Pagination: Dựa vào con trỏ (cursor) là một giá trị duy nhất để xác định điểm bắt đầu cho lượt tải kế tiếp.

    • Ưu điểm: Hiệu năng tốt hơn, phù hợp với dữ liệu động.
    • Nhược điểm: Phức tạp hơn khi triển khai.

GraphQL khuyến nghị sử dụng cursor-based pagination vì tính tối ưu và khả năng mở rộng.

2. Cách thức hoạt động của Infinite Scroll

Quy trình cơ bản của Infinite Scroll:

  1. Tải dữ liệu lần đầu khi trang được load.
  2. Theo dõi sự kiện cuộn của người dùng.
  3. Khi người dùng cuộn gần đến cuối trang, gửi truy vấn để lấy thêm dữ liệu.
  4. Cập nhật giao diện với dữ liệu mới.
  5. Dừng lại khi không còn dữ liệu.

III. Thiết lập Infinite Scroll với GraphQL

1. Chuẩn bị môi trường

Để triển khai, bạn cần có môi trường với GraphQL API và client-side library như Apollo Client.

  • Cài đặt Apollo Client:
npm install @apollo/client graphql
  • Cấu hình Apollo Client:
import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://your-graphql-endpoint.com/graphql',
  cache: new InMemoryCache()
});

2. Viết truy vấn GraphQL với Cursor-based Pagination

Ví dụ truy vấn lấy danh sách bài viết:

query GetPosts($after: String) {
  posts(first: 10, after: $after) {
    edges {
      node {
        id
        title
        content
      }
    }
    pageInfo {
      endCursor
      hasNextPage
    }
  }
}

IV. Xây dựng logic Infinite Scroll

1. Hiểu cơ chế hoạt động chi tiết

Khi triển khai Infinite Scroll với GraphQL, quy trình sẽ diễn ra theo các bước:

  1. Khởi tạo dữ liệu ban đầu: Gửi truy vấn lấy tập dữ liệu đầu tiên.
  2. Theo dõi sự kiện cuộn: Kiểm tra vị trí cuộn của người dùng.
  3. Gọi fetchMore: Khi người dùng cuộn đến gần cuối trang, gửi yêu cầu lấy thêm dữ liệu.
  4. Cập nhật danh sách: Kết hợp dữ liệu mới với dữ liệu cũ và cập nhật giao diện.
  5. Dừng tải: Khi không còn dữ liệu (hasNextPage = false).

2. Xử lý scroll mượt mà với Intersection Observer

Sử dụng Intersection Observer thay vì lắng nghe sự kiện scroll giúp tối ưu hiệu năng và đơn giản hóa việc kiểm tra khi người dùng cuộn.

Ví dụ cải tiến logic bằng Intersection Observer:

import { useQuery, gql } from '@apollo/client';
import { useEffect, useRef, useState } from 'react';

const GET_POSTS = gql`
  query GetPosts($after: String) {
    posts(first: 10, after: $after) {
      edges {
        node {
          id
          title
          content
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
`;

const InfiniteScroll = () => {
  const { data, fetchMore, loading, error } = useQuery(GET_POSTS);
  const [items, setItems] = useState([]);
  const observer = useRef<IntersectionObserver>();
  const loadMoreRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (data) {
      setItems((prev) => [...prev, ...data.posts.edges]);
    }
  }, [data]);

  useEffect(() => {
    if (!data?.posts.pageInfo.hasNextPage) return;

    observer.current = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting) {
        fetchMore({
          variables: { after: data.posts.pageInfo.endCursor },
        });
      }
    });

    if (loadMoreRef.current) {
      observer.current.observe(loadMoreRef.current);
    }

    return () => observer.current?.disconnect();
  }, [data, fetchMore]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      {items.map(({ node }) => (
        <div key={node.id}>
          <h3>{node.title}</h3>
          <p>{node.content}</p>
        </div>
      ))}
      <div ref={loadMoreRef} />
    </div>
  );
};

export default InfiniteScroll;

3. Xử lý trạng thái tải

  • Loading Indicator: Hiển thị thông báo "Đang tải..." khi gọi fetchMore().
  • Empty State: Hiển thị "Không có dữ liệu" khi danh sách rỗng.

Ví dụ thêm trạng thái tải:

{loading && <p>Đang tải dữ liệu...</p>}
{!loading && items.length === 0 && <p>Không có dữ liệu nào!</p>}
  • Sử dụng Debounce: Sử dụng debounce để hạn chế số lần gọi API khi người dùng cuộn quá nhanh
const debounce = (func, delay) => {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => func(...args), delay);
  };
};
  • Xử lý lỗi và thử lại
if (error) return <button onClick={() => fetchMore()}>Thử lại</button>;

Khi dữ liệu có thể thay đổi (ví dụ: bài viết mới được thêm), sử dụng subscribeToMore() để lắng nghe cập nhật trực tiếp.

useEffect(() => {
  const unsubscribe = subscribeToMore({
    document: POST_ADDED_SUBSCRIPTION,
    updateQuery: (prev, { subscriptionData }) => {
      if (!subscriptionData.data) return prev;
      return {
        posts: {
          ...prev.posts,
          edges: [
            subscriptionData.data.postAdded,
            ...prev.posts.edges,
          ],
        },
      };
    },
  });

  return () => unsubscribe();
}, [subscribeToMore]);
  • Trong lúc chờ dữ liệu được tải hiển thị Skeleton để cải thiện UX.
const Skeleton = () => (
  <div className="skeleton">
    <div className="skeleton-title"></div>
    <div className="skeleton-content"></div>
  </div>
);

{loading && Array.from({ length: 5 }).map((_, i) => <Skeleton key={i} />)}

V. Tối ưu hóa hiệu năng

1. Giảm thiểu dữ liệu trả về

  • Chỉ truy vấn các trường cần thiết để giảm thiểu kích thước payload.
  • Ví dụ: Không truy vấn các trường có dung lượng lớn như hình ảnh gốc, mô tả dài.

2. Sử dụng cache và Prefetching

  • InMemoryCache: Cache mặc định của Apollo, lưu trữ dữ liệu trên bộ nhớ client. Khi có truy vấn giống nhau, Apollo sẽ lấy dữ liệu từ cache thay vì gọi lại API.
  • Cache Policies: Xác định cách Apollo xử lý cache thông qua các chính sách như cache-first, network-only, cache-and-network. Ví dụ:
const { data } = useQuery(GET_POSTS, {
  fetchPolicy: 'cache-first', // Ưu tiên lấy từ cache, nếu không có thì gọi API
});
  • Pagination Cache: Sử dụng merge function để hợp nhất các kết quả phân trang, tránh mất dữ liệu cũ khi gọi fetchMore.
new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        posts: {
          keyArgs: false,
          merge(existing = [], incoming) {
            return [...existing, ...incoming];
          },
        },
      },
    },
  },
});
  • Sử dụng Prefetching để dự đoán và tải trước dữ liệu khi người dùng sắp cuộn đến phần tiếp theo.
fetchMore({ variables: { after: data.posts.pageInfo.endCursor } });
  • Xử lý lỗi:

    • Hiển thị thông báo lỗi chi tiết
    • Cho phép người dùng thử lại khi gặp lỗi mạng hoặc lỗi server
  • Hiển thị loader trong lúc load dữ liệu:

if (loading) return <p>Loading...</p>;

VI. Kết luận

Infinite Scroll kết hợp với GraphQL giúp cải thiện trải nghiệm người dùng và tối ưu hiệu năng. Với cách tiếp cận cursor-based pagination, bạn có thể xử lý lượng dữ liệu lớn mà không làm chậm ứng dụng.

Mở rộng:

  • Áp dụng lazy loading cho hình ảnh.
  • Kết hợp với các giải pháp SSR/SSG.
  • Tối ưu UX/UI với skeleton loading.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí