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

Stefan Zager
Stfan 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 kết hợp, kết thúc 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 viết này.

Blink bắt đầu hoạt động dưới dạng một nhánh của WebKit, bản thân nó là một nhánh của KHTML, bắt đầu từ năm 1998. Công cụ này chứa một số mã cũ nhất (và quan trọng nhất) trong Chromium và đến năm 2014, công cụ này đã cho thấy rõ tuổi của mình. Trong 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 tên gọi BlinkNG, với mục tiêu giải quyết những thiếu sót lâu nay trong tổ chức và cơ cấu của bộ mã Blink. Bài viết này sẽ khám phá BlinkNG và các dự án cấu thành của BlinkNG: lý do chúng tôi làm như vậy, những gì họ đã đạt được, những nguyên tắc định hình cho thiết kế của họ và 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 (kiểu), bố cục (layout), Sơn, v.v.). Tuy nhiên, các rào cản trừu tượng lại 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ó thể thay đổi và tồn tại lâu dài. Những đối tượng này có thể đã và đã được sửa đổi bất cứ lúc nào, đồng thời thường được tái chế và sử dụng lại qua các lần cập nhật kết xuất liên tiếp. Chúng tôi không thể trả lời một cách chắc chắn những câu hỏi đơn giản như:

  • Có cần cập nhật đầu ra về kiểu, bố cục hay màu vẽ không?
  • Khi nào những dữ liệu này sẽ nhận được giá trị "cuối cùng"?
  • Khi nào bạn 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ụ về vấn đề này, 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 được; trong một số trường hợp, kiểu này sẽ được sửa đổi trong các giai đoạn quy trình sau này.

Kiểu sẽ tạo một cây LayoutObject, 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í sẽ sửa đổi cấu trúc cây. Không có sự khác biệt rõ ràng giữa dữ liệu đầu vào và đầu ra của bố cục.

Kiểu sẽ tạo cấu trúc dữ liệu phụ giúp 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 chỗ theo từng giai đoạn sau kiểu.

Ở cấp độ thấp hơn, các loại dữ liệu hiển thị chủ yếu 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 thuộc tính sơn); và các giai đoạn hiển thị được triển khai dưới dạng cây đệ quy. Tốt nhất là quá trình đi bộ cây nên có phần nguyên vẹn: 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 có trước-RenderingNG; hoạt động đi bộ trên cây thường xuyên truy cập vào thông tin từ đối tượng cấp trên của nút đang được xử lý. Điều này khiến hệ thống rất dễ bị hỏng và dễ xảy ra lỗi. Cũng không thể bắt đầu đi bộ từ bất cứ đâu ngoài gốc cây.

Cuối cùng, có rất nhiều đoạn đường nối vào quy trình kết xuất được rắc khắp 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 việc nhắm mục tiêu sự kiện, cập nhật theo lịch do hệ thống hiển thị yêu cầu và các API chuyên dụng chỉ được hiển thị cho mã kiểm thử, v.v. Thậm chí có một vài đường dẫn đệ quyđệ quy dẫn đến quy trình kết xuất (nghĩa là chuyển lên đầu một giai đoạn từ giữa giai đoạn khác). Mỗi đường vào kênh 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 gồm nhiều dự án phụ, lớn và nhỏ, tất cả đều hướng đến chung một mục tiêu xoá bỏ những khiếm khuyết về mặt kiến trúc được mô tả trước đó. Trong các dự án này, chúng tôi có chung một số nguyên tắc định hướng giúp quy trình kết xuất trở nên giống với quy trình thực tế hơn:

  • Điểm truy cập thống nhất: Chúng tôi phải luôn đi vào quy trình khi bắt đầ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õ ràng, đồng thời hành vi của giai đoạn này phải có chức năng, tức là có tính xác định và có thể lặp lại; đầu ra chỉ phụ thuộc vào đầu vào đã xác định.
  • Đầu vào không đổi: Đầu vào của mọi giai đoạn phải không đổi một cách hiệu quả trong khi giai đoạn đó đang chạy.
  • Đầu ra không thể thay đổi: Sau khi một giai đoạn kết thúc, kết quả của giai đoạn đó sẽ không thể thay đổi được trong 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: Vào cuối mỗi giai đoạn, dữ liệu hiển thị được tạo ra cho đến thời điểm này phải ở 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 việc một lần.

Một 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 số hệ quả cụ thể.

Vòng đời của 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. Nhờ đó, chúng ta có thể thực hiện các hoạt động 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 vào giai đoạn vòng đời màu vẽ, 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 ra nhiều xác nhận khác xuyên suốt mã để đảm bảo chúng ta không hồi quy.

Nếu đã từng quan tâm đến mã kết xuất cấp thấp, có thể bạn sẽ tự hỏi: "Tôi đến đây bằng cách nào?" 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, dữ liệu này bao gồm các đường dẫn lệnh gọi đệ quy và đệ quy cũng như các vị trí mà chúng ta đi vào quy trình ở giai đoạn trung gian, thay vì bắt đầu từ đầu. Trong quá trình sử dụng 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 tất cả chúng đều có thể rút gọn thành hai trường hợp 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 các pixel mới để hiển thị hoặc thực hiện thử nghiệm lượt truy cập để nhắm mục tiêu theo sự kiện).
  • Chúng ta cần một giá trị mới nhất cho một truy vấn cụ thể và có thể trả lời được mà không cần cập nhật tất cả dữ liệu 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 tình huống này. Các đường dẫn mã tái lại đã bị loại bỏ hoặc tái cấu trúc, đồng thời không thể đi vào quy trình bắt đầu từ giai đoạn trung gian nữa. Điều này đã làm giảm đi rất nhiều bí ẩn về chính xác thời điểm và cách thức quá trình cập nhật kết xuất diễn ra, giúp người dùng dễ dàng hiểu được hành vi của hệ thống hơn.

Kiểu đường ống, bố cục và lớp phủ trước

Các giai đoạn kết xuất hình ảnh trước khi Vẽ nói chung chịu trách nhiệm về những điều sau:

  • Chạy thuật toán style cascade để tính toán các thuộc tính kiểu cuối cùng cho các nút DOM.
  • Đang tạo cây bố cục thể hiện hệ phân cấp hộp của tài liệu.
  • Xác định thông tin 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ụ theo toàn bộ ranh giới pixel để vẽ.
  • Xác định các thuộc tính của các lớp kết hợp (biến đổi affine, bộ lọc, độ mờ hoặc bất cứ tính năng nào khác có thể được tăng tốc GPU).
  • Xác định nội dung có thay đổi kể từ giai đoạn sơn trước đó và cần sơn hoặc sơn lại (vô hiệu hoá sơn).

Danh sách này không thay đổi, nhưng trước khi BlinkNG thực hiện nhiều công việc, công việc này được thực hiện theo một cách đặc biệt, trải rộng trên nhiều giai đoạn kết xuất, với nhiều chức năng trùng lặp và tích hợp nhiều chức năng kém hiệu quả. Ví dụ: giai đoạn style (kiểu) luôn chịu trách nhiệm chính trong 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 tôi không xác định được 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ó quan điểm chính thức hay có thể thực thi nào trong quá trình kết xuất hình ảnh nên chúng tôi có thể chắc chắn rằng thông tin kiểu là hoàn chỉnh và không thể thay đổi được.

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

Việc chèn hình học bố cục pixel con 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 nhiều lần cùng một chức năng và thực hiện rất nhiều việc dư thừa. Có một đường dẫn mã chuyển đổi điểm ảnh đượ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 mỗi khi chúng ta cần tính toán nhanh chóng, một lần về các toạ độ được ghi lại bằng pixel bên ngoài mã sơn. Không cần phải nói, mỗi cách triển khai đều 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. Vì thông tin này không được lưu vào bộ nhớ đệm nên đôi khi, hệ thống sẽ thực hiện cùng một phép tính nhiều lần – hiệu suất bị quá tải.

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

Project Squad: Xây dựng giai đoạn định hình phong cách

Dự án này đã giải quyết hai thiếu sót chính trong giai đoạn định hình khiến cho dự án không được thể hiện rõ ràng:

Có hai đầu ra chính của giai đoạn tạo kiểu: ComputedStyle (chứa kết quả chạy thuật toán tầng CSS trên cây DOM) và một cây LayoutObjects giúp thiết lập thứ tự hoạt động cho giai đoạn bố cục. Về mặt lý thuyết, việc chạy thuật toán tầng nên diễn ra nghiêm ngặt trước khi tạo cây bố cục; nhưng trước đây, hai hoạt động này được 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 tuần tự riêng biệ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 kiểu; có một vài trường hợp ComputedStyle được cập nhật trong giai đoạn quy trình sau đó. 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 tạo kiểu.

LayoutNG: Lập quy trình 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 toàn giai đoạn kết xuất bố cục. Chúng tôi sẽ không xử lý toàn bộ dự án ở đây, nhưng có một vài khía cạnh đáng chú ý đối với tổng thể dự án BlinkNG:

  • Trước đó, giai đoạn bố cục nhận cây LayoutObject do giai đoạn tạo kiểu tạo ra và chú thích cây bằng thông tin về kích thước và vị trí. Do đó, không có sự tách biệt rõ ràng giữa đầu vào và đầu ra. LayoutNG đã giới thiệu cây mảnh, là đầu ra chính, chỉ đọc của bố cục và đóng vai trò là đầu vào chính cho các giai đoạn kết xuất tiếp theo.
  • LayoutNG đã đưa thuộc tính 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 xem xét bên ngoài cây con đã bị can thiệp vào hệ thống của đố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 đều được tính toán trước và cung cấp dưới dạng dữ liệu đầu vào chỉ có thể đọc cho thuật toán.
  • Trước đây, có những trường hợp hiếm gặp mà thuật toán bố cục không hoạt động nghiêm ngặt: kết quả của thuật toán phụ thuộc vào lầ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 trước khi sơn

Trước đây, không có giai đoạn kết xuất hình ảnh trước khi tô màu chính thức, chỉ có một gói các thao tác sau khi sắp xếp bố cục. Giai đoạn sơ đồ trước xuất phát từ nhậ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 truyền tải có hệ thống của cây bố cục sau khi bố cục hoàn tất; quan trọng nhất là:

  • Cung cấp vô hiệu hoá sơn: Rất khó để vô hiệu hoá sơn một cách chính xác trong quá trình bố cục, khi chúng tôi có thông tin không đầy đủ. Sẽ dễ dàng hơn nhiều và có thể rất hiệu quả nếu nó được chia thành hai quy trình riêng biệt: trong quá trình về 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ô hiệu hóa sơn". Trong quá trình đi bộ qua cây trước khi quét, chúng tôi kiểm tra những cờ này và đưa ra trường hợp không hợp lệ nếu cần.
  • Tạo cây thuộc tính vẽ: Đây là 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: Kết quả đã ghi có thể được dùng trong giai đoạn vẽ và bất kỳ mã hạ nguồn nào cần đến kết quả mà không cần tính toán dư thừa.

Cây thuộc tính: Hình học nhất quán

Chúng tôi ra mắt cây thuộc tính từ sớm trong RenderingNG để xử lý độ phức tạp của thao tác cuộn mà 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 hệ phân cấp "lớp" duy nhất để thể hiện mối quan hệ hình học của nội dung kết hợp, nhưng mối quan hệ này nhanh chóng trở nên phức tạp khi độ phức tạp đầy đủ của các tính năng như vị trí cố định trở nên rõ ràng. Hệ phân cấp lớp có thêm các con trỏ không cục bộ cho biết thành phần mẹ cuộn (scroll parent) hoặc "clip parent" ( cha mẹ cuộn) của một lớp và trước đó rất khó để hiểu được mã.

Cây tài sản khắc phục vấn đề này bằng cách thể hiện các khía cạnh cuộn và cắt tràn của nội dung riêng 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ể mô hình hoá chính xác cấu trúc cuộn và hình ảnh 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 các 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 kết hợp hoặc xác định lớp nào được cuộn và lớp nào không.

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ã đã được nêu 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ó danh sách đầy đủ hơn.) Một vài trong số đó có cách triển khai trùng lặp giống như mã của trình tổng hợp đang thực hiện; tất cả đều có một tập hợp con lỗi khác nhau; và không có lỗi nào trong số đó được mô hình hoá đúng cách 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 ở một nơi và tái cấu trúc tất cả mã để sử dụng thuật toán đó.

Các thuật toán này đều phụ thuộc vào cây tài sản. Đó là lý do cây tài sản là một cấu trúc dữ liệu chính (chính là cấu trúc được dùng trong suốt quy trình của RenderingNG). Vì vậy, để đạt được mục tiêu của 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 vẽ trước – 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 sơn trước 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 và tạo ra 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 các cấu trúc dữ liệu đó. Chúng tôi tính toán cây thuộc tính tại đúng thời điểm khi có sẵn tất cả thông tin cần thiết; và chúng tôi đảm bảo rằng cây thuộc tính không thể thay đổi trong khi các giai đoạn hiển thị sau đó đang chạy.

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

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

Điều này gây ra những vấn đề lớn, bởi vì ít nhiều yêu cầu 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 tìm hiểu lý do thông qua một ví dụ. Giả sử chúng ta cần invalidate tô màu (có nghĩa là chúng ta cần vẽ lại danh sách hiển thị rồi sau đó quét lại). Nhu cầu vô hiệu hoá có thể xuất phát từ 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à tìm ra những lớp kết hợp nào bị ảnh hưởng và sau đó vô hiệu hóa phần hoặc tất cả 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 trước đây (trong quá khứ: có nghĩa là đối với khung kết xuất 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, khó để phân biệt sự khác biệt giữa quyết định phân lớp trong quá khứ và tương lai. Vì vậy, chúng tôi đã kết thúc với rất nhiều mã có lập 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 hoặc 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 trường hợp 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 các trường hợp, nếu mã đã cố truy vấn các quyết định phân lớp trước đây, điều đó sẽ gây ra lỗi xác nhận và gây sự cố cho trình duyệt nếu trình duyệt đang ở chế độ gỡ lỗi. Việc này đã giúp chúng tôi tránh gặp phải lỗi mới. Và trong mỗi trường hợp mã thực sự cần thiết để truy vấn các quyết định phân lớp trước đây, chúng ta sẽ đặt mã để cho phép mã bằng cách phân bổ một đối tượng DisableCompositingQueryAsserts.

Theo kế hoạch của chúng tôi, theo thời gian, chúng tôi sẽ loại bỏ tất cả đối tượng DisableCompositingQueryAssert của trang web gọi, sau đó khai báo mã là 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ể loại bỏ miễn là việc phân lớp xảy ra trước khi vẽ. (Cuối cùng, chúng tôi chỉ mới xoá gần đây!) Đây là lý do đầu tiên phát hiện ra cho dự án Tổng hợp sau khi sơn. Điều chúng tôi nhận thấy là ngay cả khi giai đoạn quy trình được xác định rõ ràng cho một hoạt động, nếu ở sai vị trí trong quy trình thì cuối cùng bạn cũng sẽ gặp khó khăn.

Lý do thứ hai dẫn đến dự án Tổng hợp sau khi vẽ là lỗi Tổng hợp cơ bản. Một cách để nói rõ lỗi này là các phần tử DOM không phải là cách thể hiện 1:1 tốt cho chương trình phân lớp hiệu quả hoặc hoàn chỉnh cho nội dung trang web. Và vì việc kết hợp có trước khi tạo, nên ít nhiều phụ thuộc vào các phần tử DOM, chứ không phải là 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 ra mắt cây thuộc tính và cũng như với cây thuộc tính, giải pháp sẽ trực tiếp biến mất nếu bạn tìm ra giai đoạn quy trình phù hợp, chạy giai đoạn đó vào đúng thời điểm và cung cấp đúng cấu trúc dữ liệu chính. Và giống như với cây thuộc tính, đây là một cơ hội tốt để đảm bảo rằng sau khi giai đoạn sơn hoàn tất, đầu ra của nó là không thể thay đổi 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 sẽ mang lại những lợi ích to lớn về lâu dài. Có nhiều hơn những gì bạn nghĩ:

  • Độ tin cậy được cải thiện đáng kể: Câu hỏi này khá đơn giản. Mã nguồn gọn gàng 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à kiểm thử hơn. Điều này giúp dữ liệu trở nên đáng tin cậy hơn. Giải pháp này cũng giúp mã an toàn và ổn định hơn, đồng thời ít gặp sự cố hơn và giảm số lỗi sau khi hết thời gian sử dụng.
  • Phạm vi kiểm thử mở rộng: Trong BlinkNG, chúng tôi đã thêm nhiều bài kiểm thử mới vào bộ công cụ này. Trong đó bao gồm các bài kiểm thử đơn vị giúp xác minh tập trung các yếu tố nội bộ; các bài kiểm thử hồi quy ngăn chúng tôi phát hành lại các lỗi cũ mà chúng tôi đã khắc phục (rất nhiều!); và nhiều nội dung bổ sung cho mọi người, được duy trì chung, Bộ kiểm thử nền tảng web mà tất cả các trình duyệt đều sử dụng để đo lường mức độ tuân thủ các tiêu chuẩn web.
  • Dễ mở rộng hơn: Nếu một hệ thống được chia nhỏ thành các thành phần rõ ràng, thì bạn không cần phải hiểu các thành phần khác ở bất kỳ mức độ chi tiết nào để có thể xử lý 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à chuyên gia chuyên 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 viết bằng mã spaghetti đã đủ khó, nhưng bạn gần như không thể đạt được những mục tiêu lớn hơn nữa như ảnh động và cuộn theo luồng chung hoặc các quy trình và luồng để tách biệt trang web nếu không có một quy trình như vậy. Tính năng 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 và chứa: BlinkNG có một số tính năng mới giúp triển khai quy trình theo những cách 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 ngân sách hết hạn? Hoặc bỏ qua bước kết xuất cho các cây con được biết là không liên quan đến người dùng hiện tại? Đó là điều mà thuộc tính CSS mức độ hiển thị nội dung cho phép. 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à tính năng rất được mong đợi sắp ra mắt trên 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 quá tuyệt vời, tại sao Google Play 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 phải hiểu và kiểm soát rất cẩn thận 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 bố trí của đối tượng cấp trên. Vì kích thước bố trí được tính toán trong quá trình bố cục, nên chúng ta cần chạy tính năng tính toán lại kiểu sau bố cục; nhưng tính năng lại kiểu sẽ chạy trước bố cục! Nghịch lý chung 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ể giải quyết vấn đề này bằng cách nào? Đó có 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ư Tổng hợp sau khi vẽ không? Thậm chí tệ hơn, nếu các kiểu mới thay đổi kích thước của đối tượng cấp trên thì sao? Vậy có phải đôi khi điều này sẽ dẫn đến một vòng lặp vô hạn không?

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

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

Đó là một điều để mô tả vùng chứa trong ngôn ngữ thông số kỹ thuật trừu tượng, và việc triển khai chính xác cũng là một vấn đề khác. Hãy nhớ lại một trong những mục tiêu của BlinkNG là đưa nguyên tắc chứa vào cây đi bộ cấu thành logic kết xuất chính: khi truyền tải một cây con, không cần phải cung cấp thông tin từ bên ngoài cây con. Như đã xảy ra (nhưng đó không chính xác là một tai nạn), nên việc triển khai vùng chứa CSS hơn nhiều sẽ gọn gàng hơn và dễ dàng hơn nếu mã hiển thị tuân thủ nguyên tắc vùng chứa.

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

Quy trình kết xuất được hiển thị tại đây thực sự đi trước một chút so với việc triển khai RenderingNG hiện tại. Phương thức này cho thấy quá trình phân lớp nằm ngoài luồng chính, trong khi hiện tại lớp này vẫn nằm trên luồng chính. Tuy nhiên, chỉ là vấn đề thời gian trước khi hoàn thành việc này. Giờ đây khi mà composite sau khi sơn đã được vận chuyển và việc phân lớp đã hoàn tất sau khi sơn.

Để hiểu tại sao điều này lại quan trọng và có thể dẫn đến những vấn đề nào khác, chúng ta cần xem xét cấu trúc của công cụ kết xuất từ một lợi thế cao hơn một chút. Một trong những trở ngại lâu dài nhất cho việc 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 phải xử lý cả logic của ứng dụng chính (tức là chạy tập lệnh) và hàng loạt kết xuất. Do đó, luồng chính thường xuyên bị bão hoà với công việc và tắc nghẽn luồng chính thường là nút thắt cổ chai trong toàn bộ trình duyệt.

Tin vui là bạn không nhất thiết phải làm như vậy! khía cạnh này trong kiến trúc của Chromium bắt nguồn từ thời KHTML, khi quá trình thực thi đơn luồng là mô hình lập trình chủ yếu. Vào thời điểm bộ xử lý đa lõi trở nên phổ biến trong các thiết bị cấp tiêu dùng, giả định đơn luồng đã được tích hợp hoàn toàn vào Blink (trước đây là WebKit). Chúng tôi đã muốn đưa thêm luồng vào công cụ kết xuất trong một thời gian dài, nhưng điều này đơn giản là không thể thực hiện được trong hệ thống cũ. Một trong những mục tiêu chính của hoạt động kết xuất NG là khai thác lỗ hổng này và giúp chúng ta có thể di 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).

BlinkNG hiện sắp hoàn tất, chúng tôi đang bắt đầu khám phá lĩnh vực này; Non-Blocking Commit là bước đột phá đầu tiên trong việc thay đổi mô hình phân luồng của trình kết xuất. commitor thread (hoặc chỉ commit) (cam kết) là một bước đồng bộ hoá giữa luồng chính và luồng của trình tổng hợp. Trong quá trình xác nhận, chúng ta sẽ tạo các bản sao của dữ liệu kết xuất được tạo trên luồng chính để dùng cho mã kết hợp xuôi dòng chạy trên luồng của trình tổng hợp. Trong khi quá trình đồng bộ hoá này 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. Bạn thực hiện việc này để đảm bảo 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 luồng đó.

Cam kết không chặn sẽ giúp 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 lệnh xác nhận chạy đồng thời trên luồng của trình tổng hợp. Kết quả thực tế của phương thức cam kết không chặn sẽ là 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, từ đó 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 của chương trình Cam kết không chặn và chúng tôi đang chuẩn bị phân tích chi tiết tác động của mô hình này đối với hiệu suất.

Chờ đợi là tính năng Kết hợp ngoài luồng chính, 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 lớp ra khỏi luồng chính và vào một luồng worker. Giống như phương thức cam kết không chặn, điều này sẽ làm giảm tình trạng tắc nghẽn trên luồng chính bằng cách giảm bớt khối lượng công việc kết xuất. 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ề mặt kiến trúc của Sau khi sơn.

Và nhiều dự án khác đang trong quá trình phát triển (dự định chơi chữ)! Cuối cùng, chúng tôi đã có 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 hình ảnh và chúng tôi rất hào hứng xem những gì có thể thực hiện!