FlatList trong React Native: hiển thị danh sách dài mà vẫn mượt

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Danh sách là thành phần xuất hiện ở hầu hết ứng dụng: bảng tin, sản phẩm, tin nhắn. Nếu làm không khéo, danh sách dài khiến ứng dụng giật và tốn bộ nhớ. FlatList của React Native được thiết kế để xử lý việc này, nhưng bạn cần dùng đúng cách.

Vì sao không dùng ScrollView cho danh sách dài?

ScrollView vẽ tất cả phần tử con ngay lập tức, kể cả những mục nằm ngoài màn hình. Với vài chục mục thì không sao, nhưng với hàng trăm hoặc hàng nghìn mục, bộ nhớ và thời gian khởi tạo tăng mạnh. FlatList chỉ vẽ các mục đang nằm trong và gần vùng nhìn thấy, đồng thời tái sử dụng tài nguyên khi cuộn.

Cách dùng cơ bản

import { FlatList, Text, View } from 'react-native';

type Item = { id: string; title: string };

export default function ProductList({ data }: { data: Item[] }) {
  return (
    <FlatList
      data={data}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View style={{ padding: 16 }}>
          <Text>{item.title}</Text>
        </View>
      )}
      ItemSeparatorComponent={() => <View style={{ height: 1, backgroundColor: '#eee' }} />}
      ListEmptyComponent={<Text>Chưa có dữ liệu</Text>}
    />
  );
}

Ba thuộc tính bắt buộc về mặt thực tế là data, renderItem và keyExtractor. Khóa phải duy nhất và ổn định; tránh dùng chỉ số mảng nếu danh sách có thể đổi thứ tự hoặc thêm xóa.

Tối ưu hiệu năng

  1. Tách item thành component riêng và bọc React.memo để không vẽ lại khi dữ liệu mục đó không đổi.
  2. Không tạo hàm hoặc đối tượng mới trong renderItem nếu có thể; dùng useCallback cho renderItem.
  3. Dùng getItemLayout khi mọi mục có chiều cao cố định, giúp cuộn tới vị trí nhanh mà không phải đo.
  4. Điều chỉnh initialNumToRender, windowSize, maxToRenderPerBatch cho phù hợp độ nặng của mục.
  5. Tối ưu ảnh: dùng kích thước ảnh vừa đủ, bộ nhớ đệm ảnh, tránh tải ảnh gốc quá lớn.
const Row = React.memo(function Row({ item }: { item: Item }) {
  return (
    <View style={{ height: 64, justifyContent: 'center', paddingHorizontal: 16 }}>
      <Text>{item.title}</Text>
    </View>
  );
});

const renderItem = useCallback(({ item }: { item: Item }) => <Row item={item} />, []);

<FlatList
  data={data}
  renderItem={renderItem}
  keyExtractor={(i) => i.id}
  getItemLayout={(_, index) => ({ length: 64, offset: 64 * index, index })}
/>

Kéo để làm mới và tải thêm

FlatList hỗ trợ sẵn hai mẫu quen thuộc. Kéo xuống để làm mới dùng refreshing và onRefresh. Tải thêm khi cuộn gần cuối dùng onEndReached với onEndReachedThreshold.

<FlatList
  data={items}
  renderItem={renderItem}
  keyExtractor={(i) => i.id}
  refreshing={refreshing}
  onRefresh={reload}
  onEndReached={loadMore}
  onEndReachedThreshold={0.5}
  ListFooterComponent={loading ? <ActivityIndicator /> : null}
/>

Hãy chặn việc gọi loadMore chồng chéo bằng cờ loading, nếu không một lần cuộn có thể kích hoạt nhiều yêu cầu cùng lúc.

Những lỗi thường gặp

  • Lồng FlatList dọc bên trong ScrollView dọc, gây cảnh báo và hiệu năng kém.
  • Không có keyExtractor nên React cảnh báo thiếu key.
  • Dữ liệu thay đổi nhưng tham chiếu mảng không đổi nên danh sách không cập nhật; hãy tạo mảng mới khi cập nhật.
  • Mục có chiều cao khác nhau mà vẫn khai báo getItemLayout cố định.

Câu hỏi thường gặp

Có nên dùng SectionList không? Có, khi dữ liệu được chia nhóm với tiêu đề từng nhóm, ví dụ danh bạ theo chữ cái.

Có thư viện nào nhanh hơn không? Có các thư viện danh sách tối ưu hóa của cộng đồng; hãy đo hiệu năng thực tế trước khi chuyển.

Kết luận

Chìa khóa của danh sách mượt là chỉ vẽ cái cần vẽ và tránh vẽ lại không cần thiết. Hãy đo bằng công cụ phân tích hiệu năng trên thiết bị thật thay vì đoán, vì thiết bị cấu hình thấp mới là nơi vấn đề lộ ra.

Đo hiệu năng danh sách thay vì đoán

Trước khi tối ưu, hãy tái hiện vấn đề trên một điện thoại cấu hình thấp ở chế độ release, vì chế độ phát triển luôn chậm hơn thực tế. Quan sát hai chỉ số: số khung hình mỗi giây của luồng giao diện và của luồng JavaScript. Nếu luồng JavaScript tụt khi cuộn, nguyên nhân thường là renderItem làm việc nặng hoặc state thay đổi quá thường xuyên khiến nhiều mục bị vẽ lại.

Một mẹo hữu ích là giữ cho dữ liệu mỗi mục nhỏ gọn và đã được chuẩn bị sẵn (ví dụ chuỗi đã định dạng) thay vì tính toán lúc vẽ. Với danh sách có ảnh, hãy dùng ảnh đã thu nhỏ từ máy chủ. Cuối cùng, thử đặt removeClippedSubviews trên Android và so sánh trước sau; không phải tùy chọn nào cũng có lợi trong mọi trường hợp, nên mọi thay đổi cần được đo lại.

Để luyện cách xây các màn hình danh sách thực tế, bạn có thể xem thêm bài học tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment