Hai mươi mục trong một nguồn cấp dữ liệu (feed) mang lại cảm giác hoàn hảo. Cuộn mượt như nhung. Khách hàng của bạn hài lòng. Thế rồi bạn triển khai lên môi trường production, dữ liệu đổ về, và đột nhiên bạn phải đối mặt với hai nghìn dòng. Giao diện (UI) bắt đầu bị giật lag. Bộ nhớ tăng dần cho đến khi hệ điều hành buộc phải đóng ứng dụng. Trong cơn tuyệt vọng, một số nhà phát triển bao bọc mọi thứ trong một ScrollView và coi như xong việc. Quyết định đó thường sinh ra ba lỗi mới cho mỗi một lỗi mà nó giải quyết được.
Danh sách là nút thắt cổ chai về hiệu suất, quyết định cách người dùng cảm nhận về ứng dụng React Native của bạn. Làm đúng, ứng dụng sẽ mang lại cảm giác mượt mà như ứng dụng gốc (native). Làm sai, ngay cả màn hình đẹp nhất cũng trở nên chậm chạp. Nguyên nhân gốc rễ thường là sự không tương thích giữa component bạn chọn và khối lượng công việc mà bạn đang yêu cầu các luồng JavaScript và UI thực hiện. React Native chạy trên hai luồng song song. Logic của bạn nằm trên luồng JS, trong khi việc vẽ (painting) diễn ra trên luồng UI native. Khi bạn render một danh sách khổng lồ không đúng cách, cả hai luồng đều bắt đầu bị quá tải bởi các tính toán bố cục (layout calculations), re-render và cấp phát bộ nhớ. Kết quả là mất khung hình (dropped frames), màn hình nhấp nháy trắng và cuối cùng là văng ứng dụng (crash).
Chọn đúng công cụ
Việc chọn một component danh sách nên là một quyết định kiến trúc có tính toán, chứ không phải là một phản xạ tự nhiên.
ScrollView là lựa chọn đơn giản nhất. Nó nhận mọi component con mà bạn cung cấp, mount từng cái một vào bộ nhớ ngay lập tức và chuyển toàn bộ khối lượng đó cho công cụ cuộn native. Đó chính xác là những gì bạn muốn cho các nội dung ngắn và cố định như màn hình cài đặt, biểu mẫu đăng nhập hoặc một trang chi tiết sản phẩm tĩnh với mười phần. Nó dễ dự đoán và dễ tạo kiểu (style). Điểm yếu là nó không có tính năng ảo hóa (virtualization). Nếu bạn đưa cho nó hai nghìn mục, nó sẽ tuân lệnh tạo ra hai nghìn native view. Đừng sử dụng ScrollView cho các tập dữ liệu lớn hoặc động. Hãy coi nó như một bức tranh đóng khung, chứ không phải một kệ sách thư viện.
FlatList là công cụ chủ lực cho các nguồn cấp dữ liệu dài và đồng nhất. Nó ảo hóa nội dung, nghĩa là nó chỉ mount những hàng hiện đang hiển thị hoặc ở gần khung hình (viewport). Khi người dùng cuộn, FlatList sẽ unmount các ô đã rời khỏi màn hình và tái sử dụng (recycle) chúng cho dữ liệu mới sắp tới. Điều này giúp mức sử dụng bộ nhớ luôn ổn định bất kể mảng dữ liệu của bạn lớn đến mức nào. Nếu bạn đang xây dựng một dòng thời gian mạng xã hội, trung tâm thông báo hoặc bất kỳ bộ sưu tập các thẻ (cards) tương tự nào có thể cuộn liên tục, FlatList là lựa chọn mặc định chính xác.
SectionList là một phiên bản FlatList có tính tổ chức cao hơn. Hãy sử dụng nó khi dữ liệu của bạn đến theo các nhóm, chẳng hạn như danh bạ được sắp xếp theo bảng chữ cái, nhật ký tập luyện được chia theo ngày, hoặc danh sách hóa đơn được tổ chức theo tháng. Nó hỗ trợ render các tiêu đề phần cố định (sticky section headers) và xử lý logic nhóm dữ liệu cho bạn. Về bản chất, nó sử dụng cùng một công cụ ảo hóa như FlatList, vì vậy bạn sẽ nhận được lợi ích về bộ nhớ tương tự cùng với cấu trúc phân chia theo tiêu đề.
FlashList xuất hiện khi bạn cần tận dụng tối đa từng khung hình cuối cùng từ thiết bị. Được xây dựng trên hệ sinh thái RecyclerListView, nó tái sử dụng các view một cách quyết liệt hơn FlatList và hướng tới việc duy trì tốc độ 60 khung hình mỗi giây ngay cả trên các thiết bị tầm trung. Nếu bạn đang xây dựng một giao diện chat có lưu lượng lớn, một danh mục sản phẩm với tốc độ cuộn nhanh, hoặc bất kỳ màn hình nào mà sự mượt mà là một lợi thế cạnh tranh, FlashList rất đáng để thêm vào như một dependency. Nó không nhất thiết phải có mặt ở mọi màn hình, nhưng đối với các nguồn cấp dữ liệu định hình nên trải nghiệm cốt lõi, sự khác biệt về hiệu suất là rất đáng kể.
Những tác nhân gây giảm hiệu suất phổ biến
Có ba "nghi phạm" thường gặp khi một danh sách bắt đầu chạy chậm.
Mount quá nhiều cây React cùng một lúc là lỗi nghiêm trọng nhất. Khi mỗi hàng là một cây component phức tạp, lần render đầu tiên có thể chặn luồng JS đủ lâu để tạo ra một màn hình trắng xóa hoặc việc hiển thị hình ảnh đầu tiên bị trễ một cách khó chịu. Người dùng mở ứng dụng và phải chờ đợi. Ngay cả sau lần tải đầu tiên, các hàng nặng nề cũng khiến việc khởi tạo cuộn trở nên chậm chạp vì vài khung hình đầu tiên đã bị tiêu tốn cho các công việc thiết lập.
Quá nhiều công việc trong mỗi khung hình sẽ biểu hiện dưới dạng hiện tượng giật (stuttering) khi cuộn. Bạn có ngân sách khoảng mười sáu mili giây cho mỗi khung hình để giữ cho các hoạt ảnh mượt mà. Nếu một component hàng thực hiện các tính toán đắt đỏ, phân tích cú pháp ngày tháng ngay khi đang chạy (on the fly), hoặc thực hiện so sánh đối tượng sâu (deep object comparisons) bên trong hàm render, bạn sẽ vượt quá ngân sách đó. Luồng UI sẽ bị mất khung hình và người dùng sẽ cảm thấy sự khựng lại.
Sử dụng bộ nhớ quá mức là kẻ giết người thầm lặng. Mỗi native view đều tiêu tốn RAM. Thêm các hình ảnh lớn chưa được tối ưu, đổ bóng (drop shadows) trên mọi thẻ, hoặc các thành phần có thể chạm lồng nhau (nested touchables) sẽ khiến mức tiêu thụ bộ nhớ nhân lên gấp bội. Trên iOS, hệ thống có thể chấm dứt ứng dụng của bạn mà không báo trước. Trên Android, người dùng sẽ thấy độ trễ tăng dần cho đến khi ứng dụng cảm thấy không thể sử dụng được nữa.
Danh sách kiểm tra tối ưu hóa
Những thói quen chiến lược nhỏ giúp phân biệt một danh sách chỉ hoạt động bình thường với một danh sách hoạt động cực kỳ mượt mà.
Sử dụng key ổn định. Luôn truyền một định danh thực tế từ tập dữ liệu của bạn vào prop key. Đừng bao giờ sử dụng chỉ số (index) của mảng. Nếu danh sách của bạn thay đổi thứ tự, lọc hoặc thêm các mục mới, một key dựa trên index sẽ đánh lừa React khiến nó ghép sai dữ liệu với component được tái sử dụng (recycled component). Sai lầm đó sẽ gây ra các lỗi unmount không cần thiết, sai lệch state và các đợt re-render dây chuyền. Một ID chuẩn xác sẽ cho React biết chính xác hàng nào đã di chuyển đến đâu.
Memoize các hàng. Bao bọc component hàng của bạn trong React.memo để nó chỉ re-render khi các props thực sự thay đổi. Nếu không có lớp bảo vệ này, bất kỳ cập nhật state nào từ component cha cũng có thể kích hoạt một đợt render qua mọi hàng đang hiển thị, ngay cả khi dữ liệu của chúng giống hệt nhau. Trong một danh sách dài liên tục biến động, những chu kỳ lãng phí đó sẽ tích tụ rất nhanh.
Giữ renderItem ổn định. Tránh định nghĩa một hàm mới trực tiếp bên trong prop renderItem sau mỗi lần render của component cha. Một arrow function inline như renderItem={({ item }) => <Row data={item} />} sẽ tạo ra một tham chiếu mới mỗi khi component cha cập nhật. FlatList sẽ thấy một prop bị thay đổi và tái sử dụng hàng một cách không cần thiết. Hãy định nghĩa hàm render bên ngoài component hoặc memoize nó bằng useCallback để tham chiếu luôn ổn định.
Sử dụng getItemLayout bất cứ khi nào có thể. Nếu các hàng của bạn có chiều cao cố định hoặc có thể dự đoán được, hãy cho FlatList biết chính xác chiều cao đó. Prop này cho phép danh sách bỏ qua các lệnh gọi đo lường native tốn kém. Thay vì đo lường từng ô sau khi mount, danh sách sẽ tính toán vị trí bằng toán học. Sự khác biệt là đặc biệt rõ rệt đối với các danh sách có hàng trăm hoặc hàng nghìn mục, nơi mà các thông báo onLayout liên tục có thể làm tê liệt JS thread.
Tối ưu hóa hình ảnh một cách quyết liệt. Hình ảnh không giới hạn kích thước là "thuốc độc" cho danh sách. Luôn thiết lập chiều rộng và chiều cao rõ ràng để lớp native có thể giữ chỗ trước khi hình ảnh được giải mã. Đối với hình ảnh từ xa, hãy sử dụng một thư viện caching như Expo Image hoặc một thư viện tương đương có khả năng xử lý bộ nhớ đệm (memory caching), lưu trữ đĩa (disk persistence) và tối ưu hóa định dạng. Component Image mặc định của React Native có thể dùng cho các bản prototype, nhưng các nguồn dữ liệu thực tế (production feeds) cần sự kiểm soát chặt chẽ hơn về bộ nhớ và trạng thái tải.
Tránh lồng các container cuộn. Đừng bao giờ đặt một FlatList dọc bên trong một ScrollView dọc. ScrollView cha sẽ chiếm quyền kiểm soát tất cả các sự kiện cuộn và làm gián đoạn khả năng đo lường viewport của FlatList con. Cơ chế ảo hóa (virtualization) sẽ bị phá vỡ vì FlatList không còn biết hàng nào cần được hiển thị. Kết quả là mọi hàng đều sẽ được mount, làm mất đi toàn bộ mục đích của việc ảo hóa. Nếu bạn cần một header phía trên danh sách, hãy sử dụng prop ListHeaderComponent của chính FlatList. Nếu bạn cần hành vi sticky phức tạp, hãy sử dụng SectionList hoặc FlashList với cấu hình header phù hợp.
Quy tắc Vàng
Nếu nội dung nhỏ và hữu hạn, hãy để ScrollView xử lý. Nếu nội dung tăng lên theo dữ liệu do người dùng tạo hoặc phân trang từ xa, hãy sử dụng một danh sách ảo hóa (virtualized list). Khi danh sách là trọng tâm của ứng dụng và người dùng sẽ cuộn trong nhiều phút liên tục, hãy sử dụng FlashList.
Đây là một sự thật cuối cùng rất dễ bị lãng quên: Những hàng "nhàm chán" sẽ cuộn rất nhanh. Component hàng càng nhẹ, danh sách của bạn càng mượt. Hãy loại bỏ các điều hướng lồng nhau, các tính toán nặng nề và các hiệu ứng hoạt ảnh không cần thiết khỏi từng hàng riêng lẻ. Giữ cho markup phẳng, logic gọn nhẹ và kích thước hình ảnh được định sẵn. Một danh sách "sống hay chết" phụ thuộc vào tổng trọng lượng của những gì nó render. Hãy làm cho mỗi hàng thật "rẻ", và danh sách sẽ mang lại cảm giác "đắt giá" theo cách tuyệt vời nhất.
