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á các phần tử ở xa khung nhìn. Sử dụng phần giữ chỗ để tính đến dữ liệu bị trễ. Sau đây là bản minh hoạ cho thanh cuộn vô hạn.

Trình cuộn vô hạn bật lên trên 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à nguồn cấp dữ liệu trực tiếp của Twitter cũng là một. Bạn di chuyển xuống và trước khi cuộn xuống dưới cùng, nội dung mới xuất hiện một cách thần kỳ dường như đột ngột. Đây là một trải nghiệm liền mạch cho người dùng và dễ thấy sự hấp dẫn.

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

Điều đúng đắnTM

Chúng tôi nghĩ rằng điều đó đã đủ lý do để đưa ra cách triển khai tệp tham chiếu cho thấy cách giải quyết tất 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 về hiệu suất.

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

Trường hợp minh hoạ của chúng ta sẽ trở thành một cửa sổ trò chuyện giống như Hangouts để chúng ta có thể cuộn qua 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ó trình cuộn vô hạn nào thực sự là vô hạn, nhưng với lượng dữ liệu có sẵn để đưa vào những trình cuộn này, chúng cũng có thể như vậy. Để đơn giản, chúng tôi 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 ngẫu nhiên cùng với một chút độ trễ nhân tạo để hoạt động giống mạng thực 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 mức để giữ cho số lượng nút DOM ở mức thấp. Ý tưởng chung là sử dụng các phần tử DOM đã tạo ở ngoài màn hình thay vì tạo các phần tử mới. Phải thừa nhận rằng bản thân các nút DOM cũng rẻ, nhưng lại không miễn phí, vì mỗi nút này sẽ làm tăng thêm chi phí về bộ nhớ, bố cục, kiểu và màu vẽ. Các thiết bị cấp thấp sẽ chậm hơn đáng kể nếu không hoàn toàn không sử dụng được nếu trang web có DOM quá lớn không thể quản lý. Ngoài ra, hãy lưu ý rằng mọi bố cục và ứng dụng lại kiểu của bạn – một quá trình được kích hoạt bất cứ khi nào một lớp được thêm vào hoặc bị xoá khỏi một nút – sẽ phát triển 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ữ cho tổng số nút DOM thấp hơn đáng kể, giúp tất cả các quy trình này nhanh hơn.

Rào cản đầu tiên là khả năng cuộn. Vì chỉ có một phần nhỏ tất cả các mục có sẵn trong DOM tại một thời điểm bất kỳ, 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ó sẵn trên lý thuyết. Chúng tôi sẽ sử dụng phần tử canh 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 tôi sẽ quảng bá mọi phần tử trên đường băng lên lớp riêng để đảm bảo lớp đó hoàn toàn trống. Không có màu nền, không có gì. Nếu lớp của đường băng không trống thì nó không đủ điều kiện để tối ưu hoá của trình duyệt và chúng ta sẽ phải lưu trữ hoạ tiết trên thẻ đồ hoạ có chiều cao vài trăm nghìn pixel. Chắc chắn là không hoạt động được trên thiết bị di động.

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

}}

Tương tự như khi cuộn theo hướng khác. Tuy nhiên, chúng tôi sẽ không bao giờ thu nhỏ đường băng trong quá trình triển khai để vị trí thanh cuộn luôn nhất quán.

Tombstone

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

Lăng mộ như vậy. Rất đá. Ồ.

Một thử thách thú vị ở đây là các mục thực có thể có chiều cao lớn hơn mục tombstone do lượng văn bản trên mỗi mục hoặc hình ảnh đính kèm không giống nhau. Để 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 có dữ liệu và tombstone được thay thế phía trên khung nhìn, neo vị trí cuộn với một phần tử thay vì giá trị pixel. Khái niệm này được gọi là neo cuộn.

Neo cho cuộn

Tính năng neo cuộn của chúng ta sẽ được gọi cả khi tombstone được thay thế cũng như khi cửa sổ đổ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ữ độ lệch từ đầu của phần tử nơi khung nhìn bắt đầu.

Cuộn sơ đồ neo.

Nếu khung nhìn được đổi kích thước và đường băng có thay đổi, chúng ta có thể khôi phục tình huống mà người dùng có cảm giác giống hệt về mặt hình ảnh. Chiến thắng! Ngoại trừ cửa sổ được đổi kích thước 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ên đặt nội dung neo xuống ở mức nào? Tuy nhiên, chúng tôi sẽ không làm như vậy. Để tìm hiểu xem, chúng ta sẽ phải bố cục mọi phần tử phía trên mục cố định và cộng tất cả chiều cao của chúng. Việc này có thể gây ra tình trạng tạm dừng đáng kể sau khi đổi kích thước mà chúng tôi không muốn điều đó. Thay vào đó, chúng ta sẽ giả định rằng mọi mục ở trên có cùng kích thước với tombstone và điều chỉnh vị trí cuộn cho phù hợp. Khi các phần tử được cuộn vào đường băng, chúng ta điều chỉnh vị trí cuộn, trì hoãn một cách hiệu quả công việc bố cục đến thời điểm 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 thấp hơn mục tiêu 60 khung hình/giây. Để tránh tình trạng này, chúng ta sẽ gánh nặng về bố cục và sử dụng các phần tử có vị trí tuyệt đối với tính năng biến đổi. Bằng cách này, chúng ta có thể giả định rằng tất cả các phần tử ở phía xa hơn đường băng vẫn đang chiếm dung lượng khi thực tế chỉ có không gian trống. Vì chúng ta đang tự thực hiện bố cục, nên chúng ta có thể lưu vào bộ nhớ đệm các vị trí nơi mỗi mục kết thúc và chúng ta 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.

Tốt nhất là các mục chỉ được vẽ lại một lần khi được đính kèm vào DOM và không bị làm mờ khi các mục thêm hoặc bị loại bỏ khác trên đường băng. Bạn có thể làm vậy, nhưng chỉ với các trình duyệt hiện đại.

Các tinh chỉnh đẹp mắt

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

Một điều khác mà chúng tôi xem xét là sử dụng IntersectionObservers làm cơ chế phát hiện thời điểm người dùng cuộn đủ lâu để chúng tôi 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ư thể sử dụng requestIdleCallback). Vì vậy, chúng ta có thể thực sự cảm thấy phản hồi chậm hơn khi dùng IntersectionObservers so với khi không có. Ngay cả cách triển khai hiện tại sử dụ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 trên cơ sở "nỗ lực tối đa". Sau cùng, Trình tổng hợp Worklet của Houdini sẽ là giải pháp có độ chân thực cao cho vấn đề này.

Vẫn chưa hoàn hảo

Cách triển khai tái chế DOM hiện tại của chúng tôi 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 phải tốn rất nhiều công sức cho bố cục và tô màu trên Chrome đến mức mà Chrome không thể theo kịp. Cuối cùng bạn sẽ không thấy gì ngoài nền. Tuy nhiên, đây không phải là nơi cuối cùng nhưng chắc chắn là điều cần cải thiện.

Chúng tôi hy vọng bạn sẽ hiểu được các vấn đề đơn giản có thể trở nên khó khăn như thế nào khi muốn kết hợp một 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 theo các hạn chế về hiệu suất.

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