Bí ẩn đằng sau độ trễ 300 ms của React.lazy và Suspense

Công nghệ29 tháng 10, 2026·6 phút đọc

Một tiện ích mở rộng trên Chrome đã phát hiện ra rằng vấn đề hiệu năng của họ không đến từ kích thước bundle mà từ cơ chế hoãn hiển thị 300 ms mặc định của React. Bài viết phân tích chi tiết cách React.lazy luôn khiến component bị suspend lần đầu và cách vô hiệu hóa cơ chế này giúp nội dung hiện ra nhanh hơn khoảng 240 ms.

Bí ẩn đằng sau độ trễ 300 ms của React.lazy và Suspense

Bí ẩn đằng sau độ trễ 300 ms của React.lazy và Suspense

Tiện ích mở rộng Tab Widgets phát hiện ra rằng mỗi lần mở tab mới, các widget phải chờ khoảng 300 ms dù mã đã tải xong từ lâu. Nguyên nhân không phải kích thước bundle quá lớn, mà là cơ chế hoãn hiển thị nội dung bị suspend có chủ đích của React. Câu chuyện này là bài học đáng giá cho bất kỳ ai đang tối ưu hiệu năng ứng dụng React.

Minh họa dòng thời gian khởi động ứng dụngMinh họa dòng thời gian khởi động ứng dụng

Vấn đề: tab mới luôn khởi động lại từ đầu

Tab Widgets là tiện ích thay thế trang tab mới, được xây dựng bằng React 19 và Rspack. Mỗi widget (đồng hồ, ghi chú, thời tiết, Gmail, Spotify...) nằm trong một chunk riêng và được tải bằng React.lazy, bọc trong một Suspense boundary riêng hiển thị placeholder trong lúc chờ.

Điểm đặc biệt của tiện ích này là không có trang "ấm" để quay về — mỗi lần mở tab là một lần khởi động từ con số không. Khi dashboard bắt đầu mất một nhịp rõ rệt để hiển thị, nhóm phát triển nghĩ ngay đến bundle quá lớn. Nhưng họ đã nhầm.

Kích thước file không phải nút thắt cổ chai

Nhóm phát triển tìm thấy một số lãng phí thực sự trong bundle khởi động. Một file index dùng chung xuất lại toàn bộ các modal, khiến modal thêm widget, cài đặt và nâng cấp bị đóng gói vào mã khởi động dù chỉ dùng khi bấm nút. Loại bỏ việc xuất lại này giúp giảm khoảng 100 KB khỏi đường khởi động.

Họ cũng chuyển thư viện ngày giờ luxon ra khỏi file chính, giảm đáng kể kích thước file 283 KB. Nhưng kết quả đo đạc cho thấy không có khác biệt nào về thời điểm nội dung xuất hiện — thư viện chỉ chuyển sang một file khác vẫn cần trong lúc khởi động. Họ đã hoàn tác thay đổi này.

File nhỏ hơn chỉ có ích nếu nó thay đổi điều trình duyệt phải chờ đợi.

Khi phân tích bản production trong Chromium headless, mọi script đều được tải và thực thi xong trong khoảng 100 ms, nhưng các widget phải đến khoảng 380 ms mới hiện ra. Trong phần lớn khoảng trống đó, trình duyệt không làm gì cả.

Hai cơ chế của React kết hợp gây bất lợi

Độ trễ đến từ hai hành vi của React:

  • Component lazy luôn bị suspend lần đầu render, kể cả khi mã đã tải xong. React.lazy chỉ gọi import() khi render lần đầu và nhận kết quả qua một promise luôn resolve sau lần render đó. Gọi import() trước cũng không giúp được gì.
  • React giới hạn tốc độ hiển thị nội dung bị suspend. Khi nội dung sẵn sàng, React chỉ hiển thị nó sau khi đã qua 300 ms kể từ lần cuối một fallback xuất hiện hoặc bị thay thế trên trang (hằng số FALLBACK_THROTTLE_MS). Mọi thứ sẵn sàng trong khoảng thời gian này sẽ hiện ra cùng lúc.

Cơ chế giới hạn này là có chủ đích: nó ngăn trang bị nhấp nháy khi nhiều phần nhỏ hoàn thành tải ở những thời điểm hơi khác nhau. Nhưng nó gây hại cho những trường hợp như Tab Widgets, nơi nội dung gần như sẵn sàng ngay lập tức.

Kết hợp hai điều trên, mỗi widget chắc chắn phải hiện placeholder và chờ ít nhất 300 ms, bất kể mã đến nhanh đến đâu. Và vì bộ đếm 300 ms dùng chung cho toàn trang, các widget còn xuất hiện theo từng đợt.

Cách khắc phục: đừng để component tự suspend

Thay vì chạy đua với bộ đếm thời gian, nhóm phát triển ngăn các widget tự suspend vì mã của chính chúng.

Mỗi widget giờ kiểm tra xem mã đã tải xong chưa. Nếu chưa, nó render placeholder như nội dung bình thường và bắt đầu tải. Khi mã đến, một cập nhật state thông thường sẽ thay thế placeholder bằng widget thật. Kỹ thuật này dựa trên một hook nhỏ, gọi _init của React trước khi render để bắt đầu tải sớm.

Nói cách khác, kỹ thuật này tận dụng các trường nội bộ của React.lazy (_init, _payload) để khởi động quá trình import trước. Nếu React thay đổi các trường này trong tương lai, component sẽ được tính là đã sẵn sàng ngay và hoạt động như cũ — một cơ chế dự phòng an toàn.

Vì React chỉ áp dụng độ trễ 300 ms khi hiển thị nội dung đã suspend, và ở đây không còn gì suspend nữa, mỗi widget sẽ xuất hiện ngay khi mã của nó được tải xong.

Kết quả đo đạc thực tế

Thử nghiệm trên Chrome chạy Windows 11, với dashboard 20 widget (16 hiển thị, đã đăng xuất):

  • Nội dung widget đầu tiên: từ 540 ms xuống 298 ms (nhanh hơn 242 ms)
  • 85% nội dung widget: từ 540 ms xuống 336 ms (nhanh hơn 204 ms)
  • Toàn bộ nội dung widget: gần như không đổi (540 ms so với 544 ms)

Hai widget Thời tiết và Word of the Day vẫn không nhanh hơn, vì chúng tải mã xong rồi suspend tiếp lần nữa trong lúc lấy dữ liệu. Giải pháp này loại bỏ việc tải mã khỏi Suspense, chứ chưa loại bỏ việc tải dữ liệu — đó là bước tiếp theo.

Bài học rút ra

  • Đo thời điểm nội dung xuất hiện, không chỉ kích thước file. Làm file chính nhỏ hơn không thay đổi thời điểm bất cứ thứ gì hiện ra.
  • Tìm khoảng thời gian nhàn rỗi. Chiến thắng lớn nhất đến từ khoảng trống mà main thread không làm gì, không phải từ việc tối ưu mã.
  • Hiểu những gì framework làm có chủ đích. Cơ chế giới hạn hiển thị của React là mặc định hợp lý để tránh nhấp nháy, nhưng trên trang khởi động lại mỗi lần và tải mã từ ổ đĩa cục bộ, chờ cố định 300 ms chính là độ trễ lớn nhất.

Với các lập trình viên Việt Nam đang xây dựng ứng dụng React, đặc biệt là các tiện ích mở rộng hoặc ứng dụng tải mã cục bộ, đây là lưu ý đáng cân nhắc: đôi khi thủ phạm không nằm ở dung lượng mà nằm ở những khoảng chờ mà ta không hề hay biết.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗