Sự phức tạp của thanh cuộn vô hạn

Tóm tắt: Sử dụng lại các phần tử DOM và xoá những phần tử nằm xa khung nhìn. Sử dụng phần giữ chỗ để tính đến dữ liệu bị trễ. Dưới đây là một bản minh hoạ cho thanh cuộn vô hạn.

Chế độ cuộn vô hạn xuất hiện trên khắp Internet. Danh sách nghệ sĩ của Google Music là một, dòng thời gian của Facebook là một và trang thông tin trực tiếp của Twitter cũng là một. Bạn cuộn xuống và trước khi đến cuối, nội dung mới xuất hiện một cách kỳ diệu, dường như từ hư không. Đây là một trải nghiệm liền mạch cho người dùng và dễ dàng thấy được sức hấp dẫn.

Tuy nhiên, thách thức kỹ thuật đằng sau một thanh cuộn vô hạn khó hơn tưởng tượng. Có rất nhiều vấn đề mà bạn gặp phải khi muốn làm The Right Thing™. Sự cố này bắt đầu từ những điều đơn giản như các đường liên kết trong chân trang thực tế không thể truy cập được vì nội dung liên tục đẩy chân trang ra xa. Nhưng các vấn đề sẽ khó khăn hơn. Bạn xử lý sự kiện đổi kích thước như thế nào khi người dùng xoay điện thoại từ chế độ dọc sang chế độ ngang hoặc làm cách nào để ngăn điện thoại bị treo khi danh sách quá dài?

The right thing™

Chúng tôi cho rằng đó là lý do đủ để đưa ra một phương thức triển khai tham chiếu cho thấy cách giải quyết tất cả các vấn đề này theo cách có thể sử dụng lại trong khi vẫn duy trì các tiêu chuẩn hiệu suất.

Chúng ta sẽ sử dụng 3 kỹ thuật để đạt được mục tiêu: tái chế DOM, mốc và neo cuộn.

Trường hợp minh hoạ của chúng ta sẽ là một cửa sổ trò chuyện giống như Hangouts, nơi chúng ta có thể cuộn qua các tin nhắn. Điều đầu tiên chúng ta cần là một nguồn tin nhắn trò chuyện vô hạn. Về mặt kỹ thuật, không có thanh cuộn vô hạn nào thực sự vô hạn, nhưng với lượng dữ liệu có sẵn để đưa vào các thanh cuộn này, chúng cũng có thể là vô hạn. Để đơn giản, chúng ta sẽ chỉ mã hoá cứng một nhóm tin nhắn trò chuyện và chọn tin nhắn, tác giả và thỉnh thoảng đính kèm hình ảnh một cách ngẫu nhiên với một chút độ trễ nhân tạo để hoạt động giống như mạng thực tế hơn một chút.

Ảnh chụp màn hình ứng dụng Chat

Tái chế DOM

Tái chế DOM là một kỹ thuật chưa được sử dụng đúng cách để giảm số lượng nút DOM. Ý tưởng chung là sử dụng các phần tử DOM đã tạo nằm ngoài màn hình thay vì tạo các phần tử mới. Mặc dù các nút DOM không tốn kém, nhưng chúng không miễn phí vì mỗi nút sẽ làm tăng thêm chi phí về bộ nhớ, bố cục, kiểu và sơn. Các thiết bị cấp thấp sẽ chậm hơn đáng kể nếu không thể sử dụng hoàn toàn nếu trang web có DOM quá lớn để quản lý. Ngoài ra, hãy lưu ý rằng mỗi lần bố cục lại và áp dụng lại kiểu – một quy trình được kích hoạt bất cứ khi nào một lớp được thêm hoặc xoá khỏi một nút – sẽ tốn kém hơn với DOM lớn hơn. Việc tái chế các nút DOM có nghĩa là chúng ta sẽ giảm đáng kể tổng số nút DOM, giúp tất cả các quy trình này diễn ra nhanh hơn.

Rào cản đầu tiên là chính việc cuộn. Vì chúng ta sẽ chỉ có một tập hợp con nhỏ của tất cả các mục có sẵn trong DOM tại một thời điểm nhất định, nên chúng ta cần tìm một cách khác để thanh cuộn của trình duyệt phản ánh chính xác lượng nội dung có trong đó theo lý thuyết. Chúng ta sẽ sử dụng phần tử sentinel 1px x 1px với một phép biến đổi để buộc phần tử chứa các mục – đường băng – có chiều cao mong muốn. Chúng ta sẽ chuyển mọi phần tử trong đường băng lên lớp riêng để đảm bảo rằng lớp của đường băng hoàn toàn trống. Không có màu nền, không có gì cả. Nếu lớp của đường băng không trống, thì lớp đó sẽ không đủ điều kiện để tối ưu hoá trình duyệt và chúng ta sẽ phải lưu trữ một hoạ tiết trên thẻ đồ hoạ có chiều cao vài trăm nghìn pixel. Chắc chắn không khả thi trên thiết bị di động.

Bất cứ khi nào cuộn, chúng ta sẽ kiểm tra xem khung nhìn có đủ gần với cuối đường băng hay không. Nếu có, chúng ta sẽ mở rộng runway bằng cách di chuyển phần tử sentinel và di chuyển các mục đã rời khỏi khung nhìn xuống cuối runway rồi điền nội dung mới vào các mục đó.

Runway Sentinel Viewport

Điều tương tự cũng xảy ra khi cuộn theo hướng khác. Tuy nhiên, chúng ta sẽ không bao giờ rút ngắn runway trong quá trình triển khai để vị trí thanh cuộn luôn nhất quán.

Bia mộ

Như đã đề cập trước đó, chúng ta cố gắng làm cho nguồn dữ liệu hoạt động giống như một thực thể trong thế giới thực. Cùng với độ trễ mạng và mọi thứ. Điều đó có nghĩa là nếu người dùng sử dụng tính năng cuộn bằng thao tác vuốt, họ có thể dễ dàng cuộn qua phần tử cuối cùng mà chúng ta có dữ liệu. Nếu điều đó xảy ra, chúng ta sẽ đặt một mục bia mộ – phần giữ chỗ – sẽ được thay thế bằng mục có nội dung thực tế sau khi dữ liệu đã đến. Hiệu ứng mộ cũng được tái chế và có một nhóm riêng cho các phần tử DOM có thể sử dụng lại. Chúng ta cần điều đó để có thể chuyển đổi một cách mượt mà từ mộ phần sang mục được điền sẵn nội dung, nếu không sẽ rất khó chịu đối với người dùng và có thể khiến họ mất dấu nội dung mà họ đang tập trung vào.

Một ngôi mộ như vậy. Rất đá. Ồ.

Một thách thức thú vị ở đây là các mục thực tế có thể có chiều cao lớn hơn mục bia mộ do số lượng văn bản khác nhau trên mỗi mục hoặc hình ảnh đính kèm. Để giải quyết vấn đề này, chúng ta sẽ điều chỉnh vị trí cuộn hiện tại mỗi khi dữ liệu đến và một mốc được thay thế phía trên khung nhìn, neo vị trí cuộn vào một phần tử thay vì giá trị pixel. Khái niệm này được gọi là neo cuộn.

Cố định cuộn

Tính năng neo cuộn của chúng ta sẽ được gọi cả khi các bia mộ đang được thay thế cũng như khi cửa sổ được đổi kích thước (điều này cũng xảy ra khi thiết bị đang được lật!). Chúng ta sẽ phải tìm ra phần tử hiển thị trên cùng trong khung nhìn. Vì phần tử đó chỉ có thể hiển thị một phần, nên chúng ta cũng sẽ lưu trữ độ dời từ đầu phần tử nơi khung nhìn bắt đầu.

Sơ đồ liên kết cuộn.

Nếu thay đổi kích thước khung nhìn và đường băng có thay đổi, chúng ta có thể khôi phục một tình huống mà người dùng cảm thấy giống hệt về mặt hình ảnh. Thắng! Ngoại trừ cửa sổ được đổi kích thước, nghĩa là mỗi mục có thể đã thay đổi chiều cao, vậy làm cách nào để chúng ta biết nội dung được neo phải được đặt ở vị trí cách xa bao nhiêu? Không! Để tìm hiểu, chúng ta sẽ phải bố trí mọi phần tử phía trên mục được neo và cộng tất cả chiều cao của các phần tử đó; việc này có thể gây ra sự tạm dừng đáng kể sau khi đổi kích thước và chúng ta không muốn điều đó. Thay vào đó, chúng ta giả định rằng mọi mục ở trên đều có cùng kích thước với bia mộ và điều chỉnh vị trí cuộn cho phù hợp. Khi các phần tử được cuộn vào runway, chúng ta sẽ điều chỉnh vị trí cuộn, trì hoãn hiệu quả công việc bố cục cho đến khi thực sự cần thiết.

Bố cục

Tôi đã bỏ qua một chi tiết quan trọng: Bố cục. Mỗi lần tái chế một phần tử DOM thường sẽ bố trí lại toàn bộ đường băng, điều này sẽ khiến chúng ta giảm xuống dưới mục tiêu 60 khung hình/giây. Để tránh điều này, chúng ta sẽ tự chịu trách nhiệm về bố cục và sử dụng các phần tử có vị trí tuyệt đối với các phép biến đổi. Bằng cách này, chúng ta có thể giả vờ rằng tất cả các phần tử ở phía trên đường băng vẫn chiếm không gian trong khi thực tế chỉ có không gian trống. Vì chúng ta tự làm bố cục, nên chúng ta có thể lưu các vị trí kết thúc của mỗi mục vào bộ nhớ đệm và có thể tải ngay phần tử chính xác từ bộ nhớ đệm khi người dùng cuộn ngược lại.

Lý tưởng nhất là các mục sẽ chỉ được vẽ lại một lần khi được đính kèm vào DOM và không bị ảnh hưởng khi các mục khác được thêm hoặc xoá trong runway. Bạn có thể làm như vậy, nhưng chỉ với các trình duyệt hiện đại.

Các bản sửa đổi mới nhất

Gần đây, Chrome đã thêm tính năng hỗ trợ CSS Containment (Giới hạn CSS). Đây là một tính năng cho phép nhà phát triển thông báo cho trình duyệt rằng một phần tử là ranh giới cho bố cục và công việc vẽ. Vì chúng ta tự tạo bố cục ở đây, nên đây là ứng dụng chính để chứa. Bất cứ khi nào thêm một phần tử vào runway, chúng ta biết rằng các mục khác không cần phải chịu ảnh hưởng của việc bố cục lại. Vì vậy, mỗi mục sẽ nhận được contain: layout. Chúng ta cũng không muốn ảnh hưởng đến phần còn lại của trang web, vì vậy, chính trang trình diễn cũng phải nhận được lệnh định kiểu này.

Một điều khác chúng tôi cân nhắc là sử dụng IntersectionObservers làm cơ chế để phát hiện thời điểm người dùng cuộn đủ xa để chúng ta bắt đầu tái chế các phần tử và tải dữ liệu mới. Tuy nhiên, IntersectionObservers được chỉ định có độ trễ cao (như khi sử dụng requestIdleCallback), vì vậy, chúng ta có thể cảm thấy IntersectionObservers kém phản hồi hơn so với khi không có. Ngay cả cách triển khai hiện tại của chúng ta bằng sự kiện scroll cũng gặp phải vấn đề này, vì các sự kiện cuộn được gửi theo cơ sở "tối đa có thể". Cuối cùng, Công cụ kết hợp Worklet của Houdini sẽ là giải pháp có độ trung thực cao cho vấn đề này.

Tuy nhiên, tính năng này vẫn chưa hoàn hảo

Cách triển khai hiện tại của chúng tôi về việc tái chế DOM không lý tưởng vì nó thêm tất cả các phần tử truyền qua khung nhìn, thay vì chỉ quan tâm đến các phần tử thực sự trên màn hình. Điều này có nghĩa là khi bạn cuộn rất nhanh, bạn đã thực hiện quá nhiều thao tác cho bố cục và vẽ trên Chrome khiến Chrome không thể theo kịp. Cuối cùng, bạn sẽ chỉ thấy nền. Đây không phải là vấn đề lớn nhưng chắc chắn là điều cần cải thiện.

Chúng tôi hy vọng bạn thấy được những vấn đề đơn giản có thể trở nên khó khăn như thế nào khi bạn muốn kết hợp trải nghiệm người dùng tuyệt vời với các tiêu chuẩn hiệu suất cao. Khi Ứng dụng web tiến bộ trở thành trải nghiệm cốt lõi trên điện thoại di động, điều này sẽ trở nên quan trọng hơn và các nhà phát triển web sẽ phải tiếp tục đầu tư vào việc sử dụng các mẫu tuân thủ các quy tắc ràng buộc về hiệu suất.

Bạn có thể tìm thấy tất cả mã trong kho lưu trữ của chúng tôi. Chúng tôi đã nỗ lực hết mình để có thể sử dụng lại thư viện này, nhưng sẽ không phát hành thư viện này dưới dạng một thư viện thực tế trên npm hoặc dưới dạng một kho lưu trữ riêng biệt. Mục đích sử dụng chính là giáo dục.