Tìm hiểu sâu về RenderingNG: BlinkNG

Stefan Zager
Stefan Zager
Chris Harrelson
Chris Harrelson

Nhấp nháy đề cập đến việc triển khai nền tảng web của Chromium và bao gồm tất cả các giai đoạn kết xuất trước khi tổng hợp, cao nhất là cam kết của trình tổng hợp. Bạn có thể đọc thêm về cấu trúc kết xuất nhấp nháy trong bài viết trước của loạt bài này.

Blink bắt đầu ra đời dưới dạng một nhánh của WebKit, chính là nhánh phát triển của KHTML có từ năm 1998. Nó chứa một số mã cũ nhất (và quan trọng nhất) trong Chromium và đến năm 2014, mã này chắc chắn đã cho thấy tuổi của nó. Năm đó, chúng tôi đã bắt tay thực hiện một loạt dự án đầy tham vọng dưới cờ hiệu của giải pháp mà chúng tôi gọi là BlinkNG, với mục tiêu giải quyết những thiếu sót lâu dài trong tổ chức và cấu trúc của mã Blink. Bài viết này sẽ giới thiệu về BlinkNG và các dự án cấu thành nên: lý do chúng tôi làm như vậy, những thành tựu của BlinkNG, những nguyên tắc định hình nên thiết kế của BlinkNG và những cơ hội cải tiến trong tương lai.

Quy trình kết xuất trước và sau BlinkNG.

Kết xuất trước NG

Về mặt lý thuyết, quy trình kết xuất trong Blink luôn được chia thành các giai đoạn (style, layout, Painter, v.v.), nhưng các rào cản trừu tượng bị rò rỉ. Nói chung, dữ liệu liên quan đến quá trình kết xuất bao gồm các đối tượng có khả năng thay đổi và tồn tại lâu dài. Các đối tượng này có thể và bị sửa đổi bất cứ lúc nào. Các đối tượng này thường xuyên được tái chế và sử dụng lại bằng cách kết xuất các bản cập nhật liên tiếp. Chúng tôi không thể trả lời một cách chính xác những câu hỏi đơn giản như:

  • Có cần cập nhật đầu ra của kiểu, bố cục hoặc lớp vẽ không?
  • Khi nào những dữ liệu này sẽ nhận được trạng thái "chính thức" giá trị?
  • Khi nào có thể sửa đổi những dữ liệu này?
  • Khi nào đối tượng này sẽ bị xoá?

Có nhiều ví dụ, bao gồm:

Kiểu sẽ tạo ComputedStyle dựa trên biểu định kiểu; nhưng ComputedStyle không thể thay đổi; trong một số trường hợp, mã này sẽ được sửa đổi ở các giai đoạn quy trình sau này.

Kiểu sẽ tạo một cây LayoutObject và sau đó bố cục sẽ chú thích các đối tượng đó bằng thông tin về kích thước và vị trí. Trong một số trường hợp, bố cục thậm chí có thể sửa đổi cấu trúc cây. Không có sự phân tách rõ ràng giữa dữ liệu đầu vào và đầu ra của bố cục.

Kiểu sẽ tạo ra các cấu trúc dữ liệu phụ kiện xác định quá trình kết hợp và các cấu trúc dữ liệu đó được sửa đổi tại mỗi giai đoạn sau khi tạo kiểu.

Ở cấp độ thấp hơn, các loại dữ liệu hiển thị phần lớn bao gồm các cây chuyên biệt (ví dụ: cây DOM, cây kiểu, cây bố cục, cây vẽ thuộc tính); và các giai đoạn kết xuất được triển khai dưới dạng lượt đi bộ đệ quy trên cây. Tốt nhất là một lượt đi bộ dạng cây nên có : khi xử lý một nút cây nhất định, chúng ta không nên truy cập vào bất kỳ thông tin nào bên ngoài cây con đã bị can thiệp vào hệ thống tại nút đó. Điều này chưa từng xảy ra trước khi kết xuấtNG; Cây đi bộ thông tin thường được truy cập từ đối tượng cấp trên của nút đang được xử lý. Điều này làm cho hệ thống trở nên rất dễ bị tổn thương và dễ gặp lỗi. Việc bắt đầu một cuộc đi bộ trên cây từ bất cứ đâu ngoài gốc của cây cũng không thể được thực hiện.

Cuối cùng, có nhiều đường dẫn vào quy trình kết xuất trong toàn bộ mã: bố cục bắt buộc được kích hoạt bởi JavaScript, cập nhật một phần được kích hoạt trong quá trình tải tài liệu, cập nhật bắt buộc để chuẩn bị cho nhắm mục tiêu theo sự kiện, cập nhật theo lịch mà hệ thống hiển thị yêu cầu và các API chuyên biệt chỉ hiển thị với mã kiểm thử, v.v. Thậm chí còn có một vài đường dẫn đệ quylặp lại trong quy trình kết xuất (nghĩa là chuyển từ đầu một giai đoạn lên đầu một giai đoạn). Mỗi đường đua này có hành vi đặc trưng riêng và trong một số trường hợp, kết quả kết xuất sẽ phụ thuộc vào cách kích hoạt quá trình cập nhật kết xuất.

Những điều chúng tôi đã thay đổi

BlinkNG bao gồm nhiều dự án phụ, lớn và nhỏ, tất cả đều có mục tiêu chung là loại bỏ những khiếm khuyết về kiến trúc được mô tả trước đó. Các dự án này có chung một số nguyên tắc hướng dẫn được thiết kế để làm cho quy trình kết xuất giống như một quy trình thực tế:

  • Điểm vào thống nhất: Chúng ta phải luôn nhập quy trình ở đầu.
  • Các giai đoạn chức năng: Mỗi giai đoạn phải có đầu vào và đầu ra được xác định rõ. Đồng thời, hành vi của giai đoạn đó phải chức năng, tức là có tính xác định và có thể lặp lại, đồng thời đầu ra chỉ nên phụ thuộc vào các đầu vào đã xác định.
  • Dữ liệu đầu vào không đổi: Dữ liệu đầu vào của bất kỳ giai đoạn nào cũng phải có giá trị cố định trong khi giai đoạn đang chạy.
  • Đầu ra bất biến: Sau khi một giai đoạn kết thúc, đầu ra của giai đoạn đó phải không thể thay đổi được đối với phần còn lại của quá trình cập nhật kết xuất.
  • Tính nhất quán của điểm kiểm tra: Ở cuối mỗi giai đoạn, dữ liệu kết xuất đã tạo từ trước đến nay sẽ ở trạng thái tự nhất quán.
  • Loại bỏ công việc trùng lặp: Chỉ tính toán mỗi thứ một lần.

Danh sách đầy đủ các dự án phụ BlinkNG sẽ khiến việc đọc trở nên tẻ nhạt, nhưng sau đây là một vài hệ quả cụ thể.

Vòng đời tài liệu

Lớp DocumentLifecycle theo dõi tiến trình của chúng ta thông qua quy trình kết xuất. Công cụ này cho phép chúng ta thực hiện các bước kiểm tra cơ bản để thực thi các bất biến được liệt kê trước đó, chẳng hạn như:

  • Nếu chúng ta đang sửa đổi một thuộc tính ComputedStyle thì vòng đời của tài liệu phải là kInStyleRecalc.
  • Nếu trạng thái DocumentLifecycle là kStyleClean trở lên, thì NeedsStyleRecalc() phải trả về false cho mọi nút đính kèm.
  • Khi bước vào giai đoạn vòng đời sơn, trạng thái vòng đời phải là kPrePaintClean.

Trong quá trình triển khai BlinkNG, chúng tôi đã loại bỏ một cách có hệ thống các đường dẫn mã vi phạm các bất biến này và đưa thêm nhiều nhận định xác nhận xuyên suốt mã để đảm bảo chúng không bị lùi lại.

Nếu bạn đã từng bước vào thế giới ngầm khi xem mã kết xuất cấp thấp, có thể bạn sẽ tự hỏi: "Làm cách nào để tôi truy cập được vào đây?" Như đã đề cập trước đó, có nhiều điểm truy cập vào quy trình kết xuất. Trước đây, điều này bao gồm các đường dẫn lệnh gọi đệ quy và lặp lại cũng như các vị trí mà chúng ta đã vào quy trình ở giai đoạn trung gian, thay vì bắt đầu từ đầu. Trong quá trình triển khai BlinkNG, chúng tôi đã phân tích các đường dẫn lệnh gọi này và xác định rằng chúng đều có thể rút gọn thành hai tình huống cơ bản:

  • Bạn cần cập nhật tất cả dữ liệu hiển thị (ví dụ: khi tạo pixel mới để hiển thị hoặc thử nghiệm lượt truy cập để nhắm mục tiêu theo sự kiện).
  • Chúng ta cần giá trị cập nhật cho một truy vấn cụ thể có thể được giải đáp mà không cần cập nhật tất cả dữ liệu về quá trình kết xuất. Điều này bao gồm hầu hết các truy vấn JavaScript, ví dụ: node.offsetTop.

Hiện chỉ có hai điểm truy cập vào quy trình kết xuất, tương ứng với hai trường hợp này. Các đường dẫn mã tái cấu trúc đã bị xoá hoặc được tái cấu trúc và không thể tiếp tục vào quy trình bắt đầu ở giai đoạn trung gian. Điều này giúp loại bỏ rất nhiều bí ẩn về thời điểm và cách thức cập nhật quá trình kết xuất, giúp bạn dễ dàng giải thích về hành vi của hệ thống.

Kiểu đường ống, bố cục và lớp sơn trước

Nói chung, các giai đoạn kết xuất trước khi Vẽ chịu trách nhiệm sau:

  • Chạy thuật toán phân tầng kiểu để tính toán các thuộc tính kiểu cuối cùng cho các nút DOM.
  • Tạo cây bố cục đại diện cho hệ phân cấp hộp của tài liệu.
  • Xác định thông tin về kích thước và vị trí cho tất cả các hộp.
  • Làm tròn hoặc chụp nhanh hình học pixel phụ vào toàn bộ đường viền pixel để vẽ.
  • Xác định thuộc tính của các lớp tổng hợp (biến đổi affine, bộ lọc, độ mờ hoặc bất kỳ yếu tố nào khác có thể được tăng tốc GPU).
  • Xác định nội dung nào đã thay đổi kể từ giai đoạn vẽ trước đó và cần được sơn hoặc sơn lại (không hợp lệ sơn).

Danh sách này không thay đổi, nhưng trước khi BlinkNG thực hiện phần lớn công việc này theo cách đặc biệt, trải rộng qua nhiều giai đoạn kết xuất, với nhiều chức năng bị trùng lặp và các hoạt động không hiệu quả được tích hợp sẵn. Ví dụ: giai đoạn style luôn chịu trách nhiệm chính về việc tính toán các thuộc tính kiểu cuối cùng cho các nút, nhưng có một vài trường hợp đặc biệt mà chúng ta không xác định giá trị thuộc tính kiểu cuối cùng cho đến khi giai đoạn style hoàn tất. Không có điểm chính thức hoặc có thể thực thi nào trong quá trình hiển thị mà chúng tôi có thể nói chắc chắn rằng thông tin về kiểu là hoàn chỉnh và không thể thay đổi.

Một ví dụ điển hình khác về sự cố trước khi BlinkNG là việc vô hiệu hoá vẽ. Trước đây, việc vô hiệu hoá lớp vẽ được trải rộng trong tất cả các giai đoạn kết xuất dẫn đến quá trình vẽ. Khi sửa đổi mã kiểu hoặc mã bố cục, bạn khó có thể biết được cần có những thay đổi nào để vẽ logic vô hiệu hoá và dễ mắc lỗi dẫn đến lỗi vô hiệu hoá dưới mức hoặc quá mức. Bạn có thể đọc thêm về những điểm phức tạp của hệ thống vô hiệu hoá sơn cũ trong bài viết của loạt bài dành cho LayoutNG.

Việc gắn hình học bố cục pixel phụ vào toàn bộ ranh giới pixel để vẽ là một ví dụ về trường hợp chúng tôi triển khai cùng một chức năng và thực hiện rất nhiều công việc dư thừa. Có một đường dẫn mã chụp nhanh pixel được hệ thống sơn sử dụng và một đường dẫn mã hoàn toàn riêng biệt được sử dụng bất cứ khi nào chúng ta cần tính toán nhanh chóng và một lần về toạ độ chụp pixel bên ngoài mã vẽ. Không cần phải nói rằng mỗi cách triển khai đều có các lỗi riêng và kết quả của chúng không phải lúc nào cũng khớp nhau. Do không có thông tin này được lưu vào bộ nhớ đệm nên đôi khi hệ thống sẽ thực hiện lặp đi lặp lại cùng một việc tính toán chính xác, một lần nữa ảnh hưởng đến hiệu suất.

Dưới đây là một số dự án quan trọng đã khắc phục những khiếm khuyết về kiến trúc của các giai đoạn kết xuất trước khi vẽ.

Project Squad: Xác định giai đoạn phong cách

Dự án này đã khắc phục hai điểm thiếu hụt chính trong giai đoạn thiết kế, điều này khiến cho dự án không được xử lý một cách dễ dàng:

Có hai đầu ra chính của giai đoạn kiểu: ComputedStyle, chứa kết quả của việc chạy thuật toán phân tầng CSS trên cây DOM; và cây LayoutObjects, giúp thiết lập thứ tự các thao tác của giai đoạn bố cục. Về mặt lý thuyết, việc chạy thuật toán phân tầng phải diễn ra nghiêm ngặt trước khi tạo cây bố cục; nhưng trước đó, hai hoạt động này bị xen kẽ. Project Squad đã thành công trong việc chia hai giai đoạn này thành các giai đoạn riêng biệt theo tuần tự.

Trước đây, không phải lúc nào ComputedStyle cũng nhận được giá trị cuối cùng trong quá trình tính toán lại định kiểu; có một vài trường hợp mà ComputedStyle được cập nhật trong giai đoạn quy trình sau này. Project Squad đã tái cấu trúc thành công các đường dẫn mã này để ComputedStyle không bao giờ bị sửa đổi sau giai đoạn định kiểu.

LayoutNG: Quy trình trong giai đoạn bố cục

Dự án hoành tráng này – một trong những nền tảng của RenderingNG – là một bản viết lại hoàn chỉnh giai đoạn kết xuất bố cục. Chúng ta sẽ không xử lý toàn bộ dự án ở đây, nhưng có một vài khía cạnh đáng chú ý trong tổng thể dự án BlinkNG:

  • Trước đây, giai đoạn bố cục nhận được cây LayoutObject do giai đoạn kiểu tạo và chú thích cây đó bằng thông tin về kích thước và vị trí. Do đó, không có sự phân tách rõ ràng giữa dữ liệu đầu vào và đầu ra. LayoutNG đã ra mắt cây mảnh. Đây là đầu ra chính, chỉ đọc của bố cục và đóng vai trò là dữ liệu đầu vào chính cho các giai đoạn kết xuất tiếp theo.
  • LayoutNG đã đưa thuộc tính ngăn chứa vào bố cục: khi tính toán kích thước và vị trí của một LayoutObject nhất định, chúng ta không còn nhìn ra bên ngoài cây con đã can thiệp vào đối tượng đó nữa. Tất cả thông tin cần thiết để cập nhật bố cục cho một đối tượng nhất định sẽ được tính toán trước và cung cấp dưới dạng đầu vào chỉ đọc cho thuật toán.
  • Trước đây, có những trường hợp hiếm gặp, trong đó thuật toán bố cục không hoạt động hoàn toàn: kết quả của thuật toán phụ thuộc vào bản cập nhật bố cục gần đây nhất trước đó. LayoutNG đã loại bỏ những trường hợp đó.

Giai đoạn chuẩn bị sơn

Trước đây, không có giai đoạn kết xuất trước khi vẽ chính thức mà chỉ có một túi chứa các hoạt động sau bố cục. Giai đoạn trước khi vẽ phát hiện ra rằng có một số chức năng liên quan có thể được triển khai tốt nhất dưới dạng di chuyển có hệ thống cây bố cục sau khi hoàn tất bố cục; quan trọng nhất:

  • Phát hành trường hợp vẽ không hợp lệ: Rất khó để thực hiện việc vô hiệu hoá lớp vẽ không hợp lệ đúng cách trong quá trình vẽ bố cục, khi chúng ta có thông tin không đầy đủ. Sẽ dễ dàng hơn nhiều và rất hiệu quả nếu được chia thành hai quy trình riêng biệt: trong quá trình định kiểu và bố cục, nội dung có thể được đánh dấu bằng một cờ boolean đơn giản là "có thể cần vẽ không hợp lệ". Trong quá trình chuẩn bị sơ đồ cây, chúng tôi sẽ kiểm tra những cờ này và đưa ra vấn đề không hợp lệ (nếu cần).
  • Tạo cây thuộc tính sơn: Một quy trình được mô tả chi tiết hơn.
  • Tính toán và ghi lại vị trí vẽ được chụp bằng pixel: Giai đoạn vẽ có thể sử dụng kết quả đã ghi và cũng có thể được sử dụng bởi bất kỳ mã hạ nguồn nào cần chúng mà không có bất kỳ tính toán thừa nào.

Cây tài sản: Hình học nhất quán

Cây thuộc tính đã ra mắt sớm trong RenderingNG để xử lý tính phức tạp của việc cuộn, do công nghệ này trên web có cấu trúc khác với tất cả các loại hiệu ứng hình ảnh khác. Trước cây thuộc tính, trình tổng hợp của Chromium sử dụng một "lớp" duy nhất hệ thống phân cấp để thể hiện mối quan hệ hình học của nội dung tổng hợp, nhưng vấn đề này nhanh chóng bị tách rời vì sự phức tạp đầy đủ của các đối tượng như vị trí:cố định trở nên rõ ràng. Hệ phân cấp lớp đã tăng thêm các con trỏ không phải cục bộ cho biết "lớp mẹ cuộn" hoặc "tạo đoạn video gốc" và trước đó, rất khó để hiểu được mã.

Cây thuộc tính đã khắc phục vấn đề này bằng cách thể hiện các khía cạnh cuộn tràn và cắt của nội dung tách biệt với tất cả các hiệu ứng hình ảnh khác. Điều này giúp bạn có thể lập mô hình chính xác cấu trúc hình ảnh và cuộn thực sự của trang web. Tiếp theo, "tất cả" chúng tôi phải làm là triển khai thuật toán trên cây thuộc tính, chẳng hạn như biến đổi không gian màn hình của các lớp tổng hợp hoặc xác định lớp nào cuộn được và lớp nào không cuộn.

Trên thực tế, chúng tôi đã sớm nhận thấy rằng có nhiều vị trí khác trong mã này đặt ra các câu hỏi hình học tương tự. (Bài đăng về cấu trúc dữ liệu chính có một danh sách đầy đủ hơn.) Một số ứng dụng trong số đó có cách triển khai trùng lặp giống như cách mà mã trình tổng hợp đang làm; đều có một nhóm lỗi khác nhau; và không có mô hình nào trong số đó lập mô hình đúng cấu trúc trang web thực sự. Sau đó, giải pháp đã trở nên rõ ràng: tập trung tất cả thuật toán hình học vào một nơi và tái cấu trúc tất cả mã để sử dụng nó.

Các thuật toán này sẽ phụ thuộc vào cây thuộc tính. Đó là lý do cây thuộc tính là một cấu trúc dữ liệu chính – tức là cấu trúc được sử dụng trong toàn bộ quy trình – của RenderingNG. Vì vậy, để đạt được mục tiêu về mã hình học tập trung này, chúng tôi cần giới thiệu khái niệm cây thuộc tính sớm hơn nhiều trong quy trình (ở giai đoạn trước khi vẽ) và thay đổi tất cả các API hiện đang phụ thuộc vào chúng để yêu cầu chạy trước khi vẽ trước khi có thể thực thi.

Câu chuyện này là một khía cạnh khác của mô hình tái cấu trúc BlinkNG: xác định các hoạt động tính toán chính, tái cấu trúc để tránh trùng lặp chúng, đồng thời tạo các giai đoạn quy trình được xác định rõ ràng để tạo cấu trúc dữ liệu cung cấp dữ liệu. Chúng tôi tính toán cây tài sản tại thời điểm chính xác có tất cả thông tin cần thiết; và đảm bảo rằng cây thuộc tính không thể thay đổi khi các giai đoạn kết xuất đang chạy sau này.

Hỗn hợp sau khi sơn: Sơn lót đường ống và kết hợp

Phân lớp là quá trình xác định nội dung DOM nào đi vào lớp tổng hợp riêng của nội dung này (từ đó đại diện cho kết cấu GPU). Trước khi RenderingNG, quá trình phân lớp chạy trước chứ không phải sau (xem tại đây để biết quy trình hiện tại – lưu ý sự thay đổi thứ tự). Trước tiên, chúng ta sẽ quyết định phần nào của DOM đi vào lớp tổng hợp nào và chỉ sau đó vẽ danh sách hiển thị cho các hoạ tiết đó. Đương nhiên, quyết định phụ thuộc vào các yếu tố như phần tử DOM nào đang tạo ảnh động hoặc cuộn hoặc có biến đổi 3D và phần tử nào được vẽ trên đó.

Điều này gây ra những vấn đề lớn vì ít nhiều cần phải có các phần phụ thuộc vòng tròn trong mã. Đây là một vấn đề lớn đối với quy trình kết xuất. Hãy cùng xem lý do thông qua một ví dụ. Giả sử chúng ta cần vô hiệu hoá sơn (có nghĩa là chúng ta cần vẽ lại danh sách hiển thị rồi tạo lại điểm ảnh). Nhu cầu vô hiệu hoá có thể là do một thay đổi trong DOM hoặc từ một kiểu hay bố cục đã thay đổi. Nhưng tất nhiên, chúng tôi chỉ muốn vô hiệu hoá những phần đã thực sự thay đổi. Điều đó có nghĩa là phải tìm ra các lớp tổng hợp nào đã bị ảnh hưởng, sau đó vô hiệu hoá một phần hoặc toàn bộ danh sách hiển thị cho các lớp đó.

Điều này có nghĩa là việc vô hiệu hoá phụ thuộc vào DOM, kiểu, bố cục và các quyết định phân lớp trong quá khứ (quá khứ: ý nghĩa đối với khung hình được hiển thị trước đó). Nhưng việc phân lớp hiện tại cũng phụ thuộc vào tất cả những yếu tố đó. Và vì chúng tôi không có hai bản sao của tất cả dữ liệu phân lớp, nên khó có thể phân biệt được sự khác biệt giữa các quyết định phân lớp trong quá khứ và trong tương lai. Vì vậy, cuối cùng chúng tôi đã tạo ra rất nhiều mã có lý luận vòng tròn. Điều này đôi khi dẫn đến mã không hợp lý hoặc không chính xác, hay thậm chí là sự cố hoặc vấn đề bảo mật, nếu chúng tôi không cẩn thận.

Để giải quyết tình huống này, chúng tôi đã giới thiệu khái niệm về đối tượng DisableCompositingQueryAsserts ngay từ đầu. Trong hầu hết trường hợp, nếu mã cố truy vấn các quyết định phân lớp trước đây, thì việc này sẽ gây ra lỗi xác nhận và sự cố trình duyệt nếu đang ở chế độ gỡ lỗi. Điều này đã giúp chúng tôi tránh tạo ra các lỗi mới. Và trong mỗi trường hợp mã cần thiết để truy vấn các quyết định phân lớp trong quá khứ, chúng ta sẽ đưa mã vào để cho phép mã đó bằng cách phân bổ đối tượng DisableCompositingQueryAsserts.

Theo thời gian, chúng tôi dự định loại bỏ tất cả đối tượng DisableCompositingQueryAssert của trang web gọi, sau đó khai báo mã an toàn và chính xác. Nhưng điều chúng tôi phát hiện ra là một số lệnh gọi về cơ bản là không thể xoá được miễn là quá trình phân lớp diễn ra trước khi vẽ. (Cuối cùng, chúng tôi mới có thể xoá thư viện mới đây!) Đây là lý do đầu tiên được phát hiện cho dự án Hỗn hợp sau khi sơn. Chúng tôi nhận ra rằng ngay cả khi bạn đã xác định rõ giai đoạn quy trình cho một hoạt động, thì nếu hoạt động đó nằm sai vị trí trong quy trình, thì cuối cùng bạn vẫn sẽ bị mắc kẹt.

Lý do thứ hai dẫn đến dự án Composite After Paint là lỗi Tổng hợp cơ bản. Một cách để nêu ra lỗi này là các phần tử DOM không phải là đại diện 1:1 tốt về một lược đồ phân lớp hiệu quả hoặc hoàn chỉnh cho nội dung trang web. Và vì quá trình kết hợp diễn ra trước khi tạo nên nó ít nhiều phụ thuộc vào các phần tử DOM, chứ không hiển thị danh sách hoặc cây thuộc tính. Điều này rất giống với lý do chúng tôi giới thiệu cây thuộc tính và cũng giống như cây thuộc tính, giải pháp sẽ trực tiếp rơi vào giai đoạn quy trình nếu bạn xác định đúng giai đoạn quy trình, chạy quy trình vào đúng thời điểm và cung cấp đúng cấu trúc dữ liệu chính. Và cũng giống như với cây thuộc tính, đây là cơ hội tốt để đảm bảo rằng sau khi giai đoạn sơn hoàn tất, kết quả của nó không thể thay đổi được cho tất cả các giai đoạn quy trình tiếp theo.

Lợi ích

Như bạn đã thấy, một quy trình kết xuất được xác định rõ ràng mang lại nhiều lợi ích to lớn về lâu dài. Thậm chí còn có nhiều tính năng khác mà bạn có thể nghĩ:

  • Độ tin cậy được cải thiện đáng kể: Câu này khá đơn giản. Mã sạch hơn với giao diện được xác định rõ ràng và dễ hiểu sẽ dễ hiểu, dễ viết và dễ kiểm thử hơn. Việc này giúp tăng độ tin cậy. Ngoài ra, API này còn giúp mã an toàn và ổn định hơn, ít gặp sự cố hơn và ít gặp lỗi không cần sử dụng hơn.
  • Phạm vi kiểm thử mở rộng: Trong quá trình triển khai BlinkNG, chúng tôi đã thêm rất nhiều quy trình kiểm thử mới vào bộ ứng dụng của mình. Chẳng hạn như kiểm thử đơn vị giúp xác minh tập trung nội bộ; kiểm tra hồi quy ngăn chúng tôi giới thiệu lại các lỗi cũ mà chúng tôi đã khắc phục (rất nhiều lỗi!); và nhiều nội dung bổ sung công khai (được duy trì chung) bộ Kiểm tra nền tảng web công khai mà tất cả trình duyệt đều sử dụng để đo lường sự tuân thủ theo tiêu chuẩn web.
  • Dễ mở rộng hơn: Nếu một hệ thống được chia thành các thành phần rõ ràng, thì bạn không cần phải hiểu rõ các thành phần khác ở bất kỳ cấp độ chi tiết nào để tiến hành thành phần hiện tại. Điều này giúp mọi người dễ dàng thêm giá trị vào mã kết xuất mà không cần phải là một chuyên gia sâu, đồng thời cũng giúp dễ dàng lý giải về hành vi của toàn bộ hệ thống.
  • Hiệu suất: Việc tối ưu hoá các thuật toán được viết bằng mã spaghetti đã khó, nhưng gần như không thể đạt được những kết quả lớn hơn nữa như hoạt động cuộn và ảnh động theo luồng phổ quát hoặc quy trình và luồng để tách biệt trang web nếu không có quy trình như vậy. Tính song song có thể giúp chúng tôi cải thiện hiệu suất rất nhiều, nhưng cũng cực kỳ phức tạp.
  • Tạo ra và ngăn chặn: BlinkNG có một số tính năng mới giúp thực hiện quy trình theo những cách thức mới mẻ. Ví dụ: điều gì xảy ra nếu chúng ta chỉ muốn chạy quy trình kết xuất cho đến khi hết ngân sách? Hoặc bỏ qua việc hiển thị đối với các cây con được xác định là không phù hợp với người dùng ngay bây giờ? Đó là tính năng mà thuộc tính CSS content- visibility mang lại. Còn việc tạo kiểu của một thành phần phụ thuộc vào bố cục của thành phần đó thì sao? Đó là truy vấn vùng chứa.

Nghiên cứu điển hình: Truy vấn vùng chứa

Truy vấn vùng chứa là một tính năng sắp ra mắt rất được mong đợi của nền tảng web (đây là tính năng được các nhà phát triển CSS yêu cầu nhiều nhất trong nhiều năm qua). Nếu dịch vụ này tuyệt vời như vậy, thì tại sao ứng dụng này vẫn chưa tồn tại? Lý do là việc triển khai truy vấn vùng chứa đòi hỏi sự hiểu biết và kiểm soát rất cẩn thận về mối quan hệ giữa kiểu và mã bố cục. Hãy cùng tìm hiểu kỹ hơn.

Truy vấn vùng chứa cho phép các kiểu áp dụng cho một phần tử phụ thuộc vào kích thước được bố trí của đối tượng cấp trên. Vì kích thước bố trí được tính toán trong bố cục, điều đó có nghĩa là chúng ta cần chạy tính toán lại kiểu sau bố cục; nhưng tính năng tính toán lại kiểu chạy trước bố cục! Nghịch lý gà và trứng này là toàn bộ lý do khiến chúng tôi không thể triển khai các truy vấn vùng chứa trước BlinkNG.

Chúng tôi có thể làm gì để giải quyết vấn đề này? Đó không phải là phần phụ thuộc quy trình ngược, tức là cùng một vấn đề mà các dự án như Composite After Paint đã được giải quyết? Thậm chí còn tệ hơn nếu kiểu mới thay đổi kích thước của đối tượng cấp trên? Chẳng phải điều này đôi khi sẽ dẫn đến một vòng lặp vô hạn phải không?

Về nguyên tắc, sự phụ thuộc vòng tròn có thể được giải quyết bằng cách sử dụng thuộc tính CSS chứa, cho phép hiển thị bên ngoài một phần tử không phụ thuộc vào việc hiển thị trong cây con của phần tử đó. Điều đó có nghĩa là các kiểu mới do vùng chứa áp dụng không thể ảnh hưởng đến kích thước của vùng chứa vì truy vấn vùng chứa yêu cầu vùng chứa.

Nhưng trên thực tế, như vậy là chưa đủ, và còn cần phải đưa ra một biện pháp ngăn chặn yếu hơn thay vì chỉ giới hạn kích thước. Điều này là do thông thường, một vùng chứa truy vấn vùng chứa sẽ có thể đổi kích thước chỉ theo một hướng (thường là khối) dựa trên kích thước cùng dòng của vùng chứa đó. Vì vậy, chúng tôi đã thêm khái niệm vùng chứa kích thước nội tuyến. Nhưng như bạn có thể thấy trong ghi chú rất dài trong phần đó, trong một thời gian dài, liệu có thể ngăn chặn kích thước cùng dòng hay không.

Để mô tả tính năng ngăn chặn bằng ngôn ngữ thông số trừu tượng, thì việc triển khai chính xác tính năng này đúng cách lại là một điều khác. Hãy nhớ rằng một trong những mục tiêu của BlinkNG là đưa nguyên tắc ngăn chặn vào các bước đi trong cây cấu thành logic chính của việc kết xuất: khi truyền tải một cây con, không cần thông tin nào từ bên ngoài cây con. Trong trường hợp đó (tốt, đây không phải chính xác là một tai nạn), việc triển khai tính năng ngăn chặn CSS sẽ rõ ràng hơn và dễ dàng hơn nếu mã hiển thị tuân thủ nguyên tắc ngăn chặn.

Tương lai: tổng hợp ngoài luồng chính ... và hơn thế nữa!

Quy trình kết xuất hiển thị tại đây thực ra sớm hơn một chút so với việc triển khai RenderingNG hiện tại. Nó hiển thị việc phân lớp khi không có trên luồng chính, trong khi hiện tại vẫn nằm trên luồng chính. Tuy nhiên, trước khi thực hiện việc này chỉ là vấn đề thời gian, vì giờ đây, hỗn hợp sau khi sơn được vận chuyển và quá trình phân lớp diễn ra sau khi sơn.

Để hiểu tại sao điều này lại quan trọng và nó có thể dẫn đến điều gì khác, chúng ta cần xem xét kiến trúc của công cụ kết xuất từ vị trí có lợi thế cao hơn một chút. Một trong những trở ngại lớn nhất để cải thiện hiệu suất của Chromium là một thực tế đơn giản là luồng chính của trình kết xuất xử lý cả logic của ứng dụng chính (tức là chạy tập lệnh) lẫn hàng loạt quá trình kết xuất. Do đó, luồng chính thường xuyên bị bão hòa công việc và tình trạng tắc nghẽn luồng chính thường là điểm tắc nghẽn trong toàn bộ trình duyệt.

Tin vui là bạn không nhất thiết phải theo cách này! Khía cạnh này trong kiến trúc của Chromium có từ thời KHTML, khi đó việc thực thi đơn luồng là mô hình lập trình chính. Vào thời điểm bộ xử lý đa nhân trở nên phổ biến trong các thiết bị cấp người dùng thông thường, giả định đơn luồng đã được đưa kỹ lưỡng vào Blink (trước đây là WebKit). Từ lâu, chúng tôi đã muốn tích hợp nhiều luồng hơn vào công cụ kết xuất, nhưng đơn giản là hệ thống cũ không thể thực hiện được điều này. Một trong những mục tiêu chính của quá trình Kết xuất NG là tìm ra lỗ hổng này để có thể chuyển một phần hoặc toàn bộ công việc kết xuất sang một luồng (hoặc luồng) khác.

Hiện tại, BlinkNG sắp hoàn tất nên chúng tôi đã bắt đầu khám phá lĩnh vực này; Cam kết không chặn là bước đột phá đầu tiên để thay đổi mô hình phân luồng của trình kết xuất. Cam kết trình tổng hợp (hoặc chỉ là cam kết) là một bước đồng bộ hoá giữa luồng chính và luồng trình tổng hợp. Trong quá trình chuyển đổi, chúng ta tạo bản sao của dữ liệu kết xuất được tạo trên luồng chính để sử dụng cho mã kết hợp hạ nguồn chạy trên luồng trình tổng hợp. Trong khi quá trình đồng bộ hoá này đang diễn ra, quá trình thực thi luồng chính sẽ dừng lại trong khi mã sao chép chạy trên luồng của trình tổng hợp. Việc này được thực hiện để đảm bảo rằng luồng chính không sửa đổi dữ liệu kết xuất trong khi luồng trình tổng hợp đang sao chép dữ liệu đó.

Cam kết không chặn sẽ giúp bạn không cần luồng chính dừng và đợi giai đoạn cam kết kết thúc – luồng chính sẽ tiếp tục hoạt động trong khi xác nhận chạy đồng thời trên luồng trình tổng hợp. Tác động ròng của Cam kết không chặn sẽ làm giảm thời gian dành riêng cho việc kết xuất công việc trên luồng chính, nhờ đó giảm tình trạng tắc nghẽn trên luồng chính và cải thiện hiệu suất. Tại thời điểm viết bài này (tháng 3 năm 2022), chúng tôi đã có một nguyên mẫu hoạt động về Cam kết không chặn và đang chuẩn bị phân tích chi tiết tác động của loại cam kết này đối với hiệu suất.

Chờ đợi cánh là Tổng hợp ngoài luồng, với mục tiêu làm cho công cụ kết xuất khớp với hình minh hoạ bằng cách di chuyển quá trình phân lớp ra khỏi luồng chính và chuyển sang luồng worker. Giống như Cam kết không chặn, tính năng này sẽ giảm tình trạng tắc nghẽn trên luồng chính bằng cách giảm khối lượng công việc kết xuất của luồng. Một dự án như thế này sẽ không bao giờ có thể thực hiện được nếu không có những cải tiến về kiến trúc của Composite After Paint.

Và vẫn còn nhiều dự án khác đang được phát triển trong tương lai! Cuối cùng, chúng tôi đã có một nền tảng để có thể thử nghiệm việc phân phối lại công việc kết xuất và chúng tôi rất vui mừng được chứng kiến khả năng đó!