Dự án lag vì 10.000 bản ghi? Đây là cách giải cứu hiệu năng!

Dự án lag vì 10.000 bản ghi? Đây là cách giải cứu hiệu năng!

Bạn đã từng "đứng hình" khi ứng dụng của mình cố gắng hiển thị một danh sách dài dằng dặc, lên đến hàng ngàn bản ghi chưa? Một kịch bản phổ biến: người dùng cuộn qua một bảng dữ liệu gồm 10.000 dòng, và đột nhiên, mọi thứ trở nên ì ạch, giật cục. Chào mừng bạn đến với thử thách tối ưu hiệu năng! Đây không chỉ là vấn đề "có thể" gặp phải mà là "chắc chắn" sẽ xảy ra với các ứng dụng xử lý dữ liệu lớn. Nhưng đừng lo, chúng ta có nhiều cách để giải quyết nó.

Tại sao 10.000 bản ghi lại là "ác mộng"?

Vấn đề chính nằm ở việc trình duyệt phải tạo và quản lý quá nhiều phần tử DOM (Document Object Model) cùng một lúc. Mỗi phần tử DOM không chỉ là một "ô vuông" trên màn hình mà còn tiêu tốn bộ nhớ, CPU cho việc render, bố cục (layout) và vẽ (paint). Với 10.000 bản ghi, bạn có thể dễ dàng có hàng chục ngàn hoặc thậm chí hàng trăm ngàn phần tử DOM nếu mỗi bản ghi có nhiều cột và phức tạp. Điều này nhanh chóng làm quá tải trình duyệt, dẫn đến tình trạng giật lag, đơ cứng.

  • Chi phí DOM cao: Mỗi node DOM đều tốn bộ nhớ và chi phí xử lý.
  • Reflow/Repaint liên tục: Khi có thay đổi nhỏ, trình duyệt phải tính toán lại bố cục và vẽ lại nhiều phần.
  • JavaScript Overhead: Logic xử lý, listener sự kiện trên quá nhiều phần tử cũng làm chậm ứng dụng.

Giải pháp vàng: Virtualization (Ảo hóa danh sách)

Đây chính là "vũ khí" mạnh mẽ nhất để đối phó với danh sách khổng lồ. Virtualization, hay còn gọi là Windowing, là kỹ thuật chỉ render và hiển thị các phần tử danh sách đang nằm trong khung nhìn (viewport) của người dùng. Các phần tử bên ngoài khung nhìn sẽ không được render, hoặc được "gỡ bỏ" khỏi DOM và tái sử dụng khi cuộn đến.

Virtualization là gì?

Thay vì render tất cả 10.000 dòng, chúng ta chỉ render 20-50 dòng mà người dùng đang nhìn thấy. Khi người dùng cuộn, các dòng đã đi qua sẽ bị ẩn đi, và các dòng mới sắp xuất hiện sẽ được render.

Cách thức hoạt động

Giả sử bạn có một danh sách 10.000 mục, nhưng khung hình chỉ hiển thị được 20 mục cùng lúc. Kỹ thuật Virtualization sẽ:

  1. Tính toán số lượng mục có thể hiển thị trong khung nhìn.
  2. Chỉ render các mục đó cùng với một vài mục "đệm" ở trên và dưới để tạo hiệu ứng cuộn mượt mà.
  3. Khi người dùng cuộn, nó sẽ phát hiện vị trí cuộn, gỡ bỏ các mục không còn trong khung nhìn và render các mục mới.

Điều này giúp giảm đáng kể số lượng phần tử DOM mà trình duyệt phải quản lý, từ 10.000 xuống chỉ còn khoảng 20-30 phần tử.

Khi nào nên dùng?

  • Khi bạn cần hiển thị một danh sách rất dài (hàng trăm, hàng ngàn mục) và người dùng cần khả năng cuộn tự do qua toàn bộ danh sách.
  • Khi hiệu năng cuộn là ưu tiên hàng đầu.

Thư viện phổ biến

Thay vì tự triển khai (khá phức tạp), bạn nên sử dụng các thư viện có sẵn:

  • React: react-window, react-virtualized
  • Vue: vue-virtual-scroller
  • Angular: @angular/cdk/scrolling

Ví dụ với react-window (conceptual):

import { FixedSizeList } from 'react-window'; const Row = ({ index, style }) => ( <div style={style}> Dòng số {index} </div> ); const MyBigList = () => ( <FixedSizeList height={500} width={800} itemCount={10000} itemSize={50} // Chiều cao mỗi dòng > {Row} </FixedSizeList> ); export default MyBigList;

Các phương án bổ trợ và thay thế

Phân trang (Pagination)

Đây là kỹ thuật cơ bản nhất: chia danh sách lớn thành các trang nhỏ hơn. Mỗi trang chỉ hiển thị một số lượng bản ghi giới hạn (ví dụ: 10, 20, 50). Người dùng phải click để chuyển sang trang khác.

  • Ưu điểm: Dễ triển khai, kiểm soát tốt số lượng DOM.
  • Nhược điểm: Người dùng phải click nhiều lần để xem hết dữ liệu, không tối ưu cho trải nghiệm duyệt nhanh.
  • Khi nào dùng: Khi dữ liệu có cấu trúc trang rõ ràng, hoặc khi người dùng thường chỉ quan tâm đến một phần nhỏ dữ liệu.

Cuộn vô hạn (Infinite Scroll)

Tương tự như Facebook hay Twitter, khi người dùng cuộn đến cuối danh sách hiện có, ứng dụng sẽ tự động tải thêm dữ liệu và nối vào danh sách. Nó mang lại cảm giác cuộn liên tục hơn phân trang.

  • Ưu điểm: Trải nghiệm người dùng mượt mà hơn phân trang.
  • Nhược điểm: Dễ bị quá tải DOM nếu không kết hợp với Virtualization, khó quay lại vị trí cũ.
  • Khi nào dùng: Khi người dùng có xu hướng duyệt liên tục, khám phá nội dung mới.

Tối ưu hóa Component (React.memo, shouldComponentUpdate)

Nếu các item trong danh sách của bạn là các component phức tạp, việc chúng re-render không cần thiết cũng là một nguyên nhân gây chậm. Trong React, bạn có thể dùng React.memo (cho functional components) hoặc shouldComponentUpdate (cho class components) để ngăn chúng re-render nếu props hoặc state của chúng không thay đổi.

Ví dụ với React.memo:

import React from 'react'; const ListItem = React.memo(({ itemData }) => { // Component này chỉ re-render nếu itemData thay đổi return ( <div> <h3>{itemData.title}</h3> <p>{itemData.description}</p> </div> ); }); export default ListItem;

Đừng quên công cụ "thám tử" của bạn: Performance Profiling

Trước khi lao vào tối ưu, hãy luôn luôn profile ứng dụng của bạn bằng các công cụ như Chrome DevTools (tab Performance). Điều này giúp bạn xác định chính xác "nút thắt cổ chai" gây ra tình trạng chậm trễ. Có thể vấn đề không phải ở DOM mà là ở một đoạn tính toán JavaScript phức tạp nào đó!

  • Ghi lại hoạt động: Sử dụng tab Performance để ghi lại quá trình cuộn, tải dữ liệu.
  • Phân tích biểu đồ Flame Chart: Tìm kiếm các tác vụ dài, đặc biệt là phần "Rendering", "Scripting".
  • Kiểm tra số lượng DOM: Tab Elements hoặc Performance cũng hiển thị số lượng DOM nodes.

Lời kết

Giải quyết vấn đề hiệu năng với danh sách lớn không phải là chuyện ngày một ngày hai, nhưng cũng không phải là "nhiệm vụ bất khả thi". Bằng cách hiểu rõ nguyên nhân gốc rễ và áp dụng các kỹ thuật như Virtualization, phân trang, cuộn vô hạn, và tối ưu hóa component, bạn hoàn toàn có thể biến trải nghiệm "lag" thành "mượt mà" đáng kinh ngạc. Đừng quên profiling để luôn đi đúng hướng nhé! Chúc bạn thành công!