Blink đề 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 bằng cam kết của trình kết hợp. Bạn có thể đọc thêm về kiến trúc kết xuất blink trong bài viết trước đó trong loạt bài này.
Blink bắt đầu là một nhánh của WebKit, bản thân WebKit cũng là một nhánh của KHTML từ năm 1998. Thư viện này chứa một số mã lâu đời nhất (và quan trọng nhất) trong Chromium, và đến năm 2014, thư viện này đã bắt đầu có dấu hiệu lỗi thời. Trong năm đó, chúng tôi đã bắt tay vào một loạt dự án đầy tham vọng với tên gọi BlinkNG, nhằm mục đích giải quyết những thiếu sót lâu nay trong việc sắp xếp và cấu trúc mã Blink. Bài viết này sẽ khám phá BlinkNG và các dự án thành phần của nó: lý do chúng tôi thực hiện các dự án này, những gì chúng đã đạt được, các nguyên tắc hướng dẫn định hình thiết kế của chúng và những cơ hội cải tiến trong tương lai mà chúng mang lại.
Kết xuất trước NG
Quy trình kết xuất trong Blink luôn được chia thành các giai đoạn (kiểu, bố cục, vẽ, v.v.) theo khái niệm, nhưng các rào cản trừu tượng lại bị rò rỉ. Nói chung, dữ liệu liên kết với quá trình kết xuất bao gồm các đối tượng có thời gian tồn tại lâu dài và có thể thay đổi. Các đối tượng này có thể được sửa đổi (và đã được sửa đổi) bất cứ lúc nào, đồng thời thường xuyên được tái chế và sử dụng lại thông qua các bản cập nhật kết xuất liên tiếp. Không thể trả lời một cách đáng tin cậy các câu hỏi đơn giản như:
- Bạn có cần cập nhật kết quả của kiểu, bố cục hoặc sơn không?
- Khi nào 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ề việc này, bao gồm:
Kiểu sẽ tạo ComputedStyle
dựa trên các tệp kiểu; nhưng ComputedStyle
không phải là bất biến; trong một số trường hợp, ComputedStyle
sẽ được sửa đổi theo các giai đoạn quy trình sau.
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ự 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 các cấu trúc dữ liệu phụ 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 kết xuất 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 kết xuất được triển khai dưới dạng các lượt duyệt cây đệ quy. Tốt nhất là một cây đi bộ phải được đưa vào: khi xử lý một nút cây nhất định, chúng ta không được truy cập vào bất kỳ thông tin nào bên ngoài cây con bắt nguồn từ nút đó. Điều đó chưa bao giờ đúng trước RenderingNG; các bước đi trên cây thường xuyên truy cập thông tin từ các thành phần 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ị lỗi và dễ hỏng. Bạn cũng không thể bắt đầu một cây đi bộ từ bất cứ đâu, ngoại trừ gốc cây.
Cuối cùng, có nhiều điểm khởi đầu vào quy trình kết xuất được rải rác trong mã: bố cục bắt buộc do JavaScript kích hoạt, một số nội dung cập nhật đượ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 tính năng 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 biệt chỉ hiển thị cho mã kiểm thử, v.v. Thậm chí còn có một số đường dẫn recursion (lặp lại) và reentrant (lặp lại) vào quy trình kết xuất (tức là chuyển đến đầu một giai đoạn từ giữa một giai đoạn khác). Mỗi bước khởi động này đều có hành vi riêng biệt 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 bản cập nhật kết xuất.
Những điểm thay đổi
BlinkNG bao gồm nhiều dự án phụ, lớn và nhỏ, tất cả đều có chung mục tiêu là loại bỏ các thiếu sót về cấu trúc được mô tả trước đó. Các dự án này có chung một số nguyên tắc định hướng được thiết kế để giúp quy trình kết xuất trở thành quy trình thực tế hơn:
- Điểm truy cập đồng nhất: Chúng ta phải luôn bắt đầu quy trình từ đầu.
- 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 đó phải chức năng, tức là có thể xác định trước và lặp lại, đồng thờ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 bất kỳ giai đoạn nào 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, đầu ra của giai đoạn đó sẽ không thể thay đổi 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: Ở cuối mỗi giai đoạn, dữ liệu kết xuất được tạo cho đến thời điểm đó phải ở trạng thái tự nhất quán.
- Loại bỏ trùng lặp công việc: Chỉ tính toán mỗi việc một lần.
Danh sách đầy đủ các dự án phụ BlinkNG sẽ khiến bạn đọc thấy nhàm chán, nhưng sau đây là một số hậu 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. Hàm 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 hằng số được liệt kê trước đó, chẳng hạn như:
- Nếu chúng ta đang sửa đổi thuộc tính ComputedStyle, thì vòng đời 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 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 hằng số này và thêm nhiều câu nhận định khác vào mã để đảm bảo không bị hồi quy.
Nếu từng tìm hiểu về mã kết xuất cấp thấp, bạn có thể tự hỏi: "Làm cách nào để đến đâ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à tái nhập, cũng như những vị trí chúng ta đã nhập vào quy trình ở giai đoạn trung gian, thay vì bắt đầu từ đầu. Trong quá trình phát triển 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ả đề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 kết xuất, ví dụ: khi tạo pixel mới để hiển thị hoặc thực hiện kiểm thử lượt nhấp cho tính năng nhắm mục tiêu sự kiện.
- Chúng ta cần một giá trị mới nhất cho một truy vấn cụ thể có thể được trả lời 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 tại, 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ã có thể truy cập lại đã bị xoá hoặc tái cấu trúc và bạn không thể nhập quy trình bắt đầu từ giai đoạn trung gian nữa. Điều này đã loại bỏ nhiều bí ẩn về thời điểm và cách thức cập nhật kết xuất, giúp bạn dễ dàng lý giải hành vi của hệ thống hơn.
Kiểu, bố cục và vẽ trước trong quy trình tạo luồng
Nhìn chung, các giai đoạn kết xuất trước khi vẽ chịu trách nhiệm về những việc sau:
- Chạy thuật toán style cascade (loạt kiểu) để tính 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 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 điểm ảnh phụ vào ranh giới toàn bộ điểm ảnh để vẽ.
- Xác định các thuộc tính của lớp kết hợp (biến đổi affine, bộ lọc, độ mờ hoặc bất kỳ thuộc tính nào khác có thể 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 vẽ hoặc vẽ lại (vô hiệu hoá quá trình vẽ).
Danh sách này không thay đổi, nhưng trước BlinkNG, phần lớn công việc này được thực hiện theo cách đặc biệt, trải dài trên nhiều giai đoạn kết xuất, với nhiều chức năng trùng lặp và kém hiệu quả tích hợp. 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 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 (kiểu) 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 kết xuất mà chúng ta có thể chắc chắn rằng thông tin kiểu đã hoàn chỉnh và không thể thay đổi.
Một ví dụ khác về sự cố trước BlinkNG là vô hiệu hoá 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 sơn. Khi sửa đổi mã kiểu hoặc bố cục, bạn khó biết cần thay đổi những gì để vẽ logic vô hiệu hoá và dễ mắc lỗi dẫn đến lỗi vô hiệu hoá quá mức hoặc không đủ. Bạn có thể đọc thêm về những chi tiết phức tạp của hệ thống vô hiệu hoá sơn cũ trong bài viết thuộc loạt bài này dành riêng cho LayoutNG.
Việc chụp nhanh hình học bố cục điểm ảnh phụ vào ranh giới toàn bộ điểm ảnh để vẽ là một ví dụ về việc chúng ta đã triển khai nhiều chức năng tương tự và thực hiện nhiều công việc thừa. Hệ thống vẽ sử dụng một đường dẫn mã chụp nhanh pixel và một đường dẫn mã hoàn toàn riêng biệt được dùng bất cứ khi nào chúng ta cần tính toán nhanh và một lần các toạ độ chụp nhanh pixel bên ngoài mã vẽ. Không cần phải nói, mỗi phương thức 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ì không lưu thông tin này vào bộ nhớ đệm, nên đôi khi hệ thống sẽ thực hiện lại chính xác cùng một phép tính – một yếu tố khác gây ảnh hưởng đến hiệu suất.
Sau đây là một số dự án quan trọng đã loại bỏ các thiếu sót về cấu trúc của các giai đoạn kết xuất trước khi vẽ.
Project Squad: Luồng xử lý giai đoạn kiểu
Dự án này đã giải quyết hai vấn đề chính trong giai đoạn kiểu khiến dự án không thể được tạo quy trình một cách rõ ràng:
Có hai đầu ra chính của giai đoạn tạo kiểu: ComputedStyle
, chứa kết quả của việc chạy thuật toán CSS dạng thác trên cây DOM; và một cây LayoutObjects
, thiết lập thứ tự các thao tác cho giai đoạn bố cục. Về mặt khái niệm, việc chạy thuật toán thác nước phải diễn ra nghiêm ngặt trước khi tạo cây bố cục; nhưng trước đây, hai thao tác 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, ComputedStyle
không phải lúc nào 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 đó. Nhóm dự án đã tái cấu trúc thành công các đường dẫn mã này để ComputedStyle
không bao giờ được sửa đổi sau giai đoạn kiểu.
LayoutNG: Luồng xử lý giai đoạn bố cục
Dự án đồ sộ này – một trong những nền tảng của RenderingNG – là bản viết lại hoàn toàn của giai đoạn kết xuất bố cục. Chúng tôi sẽ không đề cập đến toàn bộ dự án ở đây, nhưng có một vài khía cạnh đáng chú ý đối với dự án BlinkNG tổng thể:
- Trước đây, giai đoạn bố cục nhận được một cây
LayoutObject
do giai đoạn kiểu tạo và chú thích cây bằng thông tin kích thước và vị trí. Do đó, không có sự tách biệt rõ ràng giữa dữ liệu đầu vào và dữ liệu đầu ra. LayoutNG đã giới thiệu cây mảnh, đây là đầu ra chính, chỉ có thể đọ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 vùng 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 bên ngoài cây con bắt nguồn từ đối tượng đó. 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 được tính toán trước và được 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ó một số trường hợp đặc biệt mà thuật toán bố cục không hoạt động đúng cách: 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 trước khi vẽ
Trước đây, không có giai đoạn kết xuất trước khi vẽ chính thức, chỉ có một túi lấy các thao tác sau bố cục. Giai đoạn vẽ trước xuất phát từ việc nhận thấy rằng có một số hàm liên quan có thể được triển khai tốt nhất dưới dạng một quá trình duyệt 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à:
- Xác nhận vô hiệu hoá sơn: Rất khó để xác nhận vô hiệu hoá sơn một cách chính xác trong quá trình bố cục, khi chúng ta có thông tin chưa đầy đủ. Việc này sẽ dễ dàng hơn nhiều và có thể rất hiệu quả nếu được chia thành hai quy trình riêng biệt: trong quá trình tạo 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 hoá sơn". Trong quá trình duyệt cây trước khi vẽ, chúng ta kiểm tra các cờ này và phát hành các yêu cầu vô hiệu hoá 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 ở phần sau.
- Tính toán và ghi lại vị trí vẽ được chụp nhanh bằng pixel: Giai đoạn vẽ có thể sử dụng kết quả đã ghi lại, cũng như bất kỳ mã nào ở hạ nguồn cần đến kết quả đó mà không cần tính toán thừa.
Cây thuộc tính: Hình học nhất quán
Cây thuộc tính được giới thiệu sớm trong RenderingNG để xử lý độ phức tạp của thao tác cuộn. Thao tác 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 khi 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 để biểu thị mối quan hệ hình học của nội dung tổng hợp, nhưng điều đó nhanh chóng bị phá vỡ khi toàn bộ sự phức tạp của các tính năng như position:fixed trở nên rõ ràng. Hệ phân cấp lớp đã phát triển thêm các con trỏ không cục bộ cho biết "mẹ cuộn" hoặc "mẹ cắt" của một lớp, và chẳng bao lâu sau, rất khó để hiểu 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 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 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ả" những gì chúng ta phải làm là triển khai các thuật toán trên đầu cây thuộc tính, chẳng hạn như phép 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 cuộn và lớp nào không.
Trên thực tế, chúng ta sớm nhận thấy rằng có nhiều vị trí khác trong mã cũng đặ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ó danh sách đầy đủ hơn.) Một số trong số đó có cách triển khai trùng lặp cho cùng một việc mà mã trình kết 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ó mã nào mô hình hoá đú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ả cá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 lần lượt phụ thuộc vào cây thuộc tính, đó là lý do tại sao 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 này về mã hình học tập trung, chúng ta cần giới thiệu khái niệm về cây thuộc tính sớm hơn nhiều trong quy trình – trong quá trình vẽ trước – và thay đổi tất cả các API hiện phụ thuộc vào chúng để yêu cầu chạy quá trình vẽ 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ẫu tái cấu trúc BlinkNG: xác định các phép tính chính, tái cấu trúc để tránh trùng lặp các phép tính đó và 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 cho các phép tính đó. Chúng tôi tính toán cây thuộc tính tại chính xác thời điểm có tất cả thông tin cần thiết; đồng thời đảm bảo rằng cây thuộc tính không thể thay đổi trong khi các giai đoạn kết xuất sau đó đang chạy.
Kết hợp sau khi vẽ: Quy trình vẽ và kết hợp
Lớp hoá là quá trình xác định nội dung DOM nào sẽ đi vào lớp kết hợp riêng của nó (đổi lại, lớp này đại diện cho hoạ tiết GPU). Trước RenderingNG, quá trình phân lớp chạy trước khi vẽ, chứ không phải sau (xem tại đây để biết quy trình hiện tại – lưu ý thay đổi thứ tự). Trước tiên, chúng ta sẽ quyết định phần nào của DOM sẽ đi vào lớp kết hợp nào, sau đó mới vẽ danh sách hiển thị cho các hoạ tiết đó. Đương nhiên, các quyết định này 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ó phép biến đổi 3D và phần tử nào được vẽ lên trên phần tử nào.
Điều này đã gây ra các vấn đề lớn, vì ít nhiều cũng yêu cầu 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 xem lý do thông qua một ví dụ. Giả sử chúng ta cần vô hiệu hoá quá trình vẽ (nghĩa là chúng ta cần vẽ lại danh sách hiển thị rồi quét lại). Việc cần vô hiệu hoá có thể đến từ một thay đổi trong DOM hoặc từ một kiểu hoặc bố cục đã thay đổi. Nhưng tất nhiên, chúng ta 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 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 trước đây (trước đây: có nghĩa là đối với khung hiển thị trước đó). Tuy nhiên, 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 rất khó để phân biệt sự khác biệt giữa các quyết định phân lớp trước đây và trong tương lai. Vì vậy, chúng tôi đã có rất nhiều mã có lý luận vòng tròn. Đôi khi, điều này dẫn đến mã không hợp lý hoặc không chính xác, thậm chí là sự cố hoặc vấn đề bảo mật nếu chúng ta không cẩn thận.
Để xử lý tình huống này, ngay từ đầu, chúng ta đã giới thiệu khái niệm về đối tượng DisableCompositingQueryAsserts
. Trong hầu hết trường hợp, nếu mã cố gắng truy vấn các quyết định phân lớp trước đó, thì mã đó sẽ gây ra lỗi xác nhận và làm trình duyệt gặp sự cố nếu trình duyệt đang ở chế độ gỡ lỗi. Điều này giúp chúng tôi tránh được việc đưa ra các lỗi mới. Và trong mỗi trường hợp mã cần hợp pháp để truy vấn các quyết định phân lớp trước đây, chúng ta sẽ đưa mã vào để cho phép 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ả các đối tượng DisableCompositingQueryAssert
của vị trí gọi, sau đó khai báo mã an toàn và chính xác. Tuy nhiên, chúng tôi phát hiện ra rằng về cơ bản, không thể xoá một số lệnh gọi miễn là quá trình phân lớp xảy ra trước khi vẽ. (Cuối cùng chúng tôi cũng có thể xoá tệp đó vào thời gian gần đây!) Đây là lý do đầu tiên được phát hiện cho dự án Composite After Paint. Chúng tôi nhận thấy rằng ngay cả khi bạn xác định rõ giai đoạn quy trình cho một thao tác, nếu thao tác đó nằm sai vị trí trong quy trình, thì cuối cùng bạn sẽ gặp sự cố.
Lý do thứ hai cho dự án Composite After Paint là lỗi kết hợp cơ bản. Một cách để nêu lỗi này là các phần tử DOM không thể hiện chính xác tỷ lệ 1:1 của một lược đồ phân lớp hiệu quả hoặc đầy đủ cho nội dung trang web. Và vì quá trình kết hợp diễn ra trước khi vẽ, nên quá trình này ít nhiều phụ thuộc vào các phần tử DOM, chứ không phải danh sách hiển thị 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. Cũng giống như cây thuộc tính, giải pháp sẽ xuất hiện trực tiếp nếu bạn tìm ra giai đoạn quy trình phù hợp, chạy quy trình đó vào đúng thời điểm và cung cấp cho quy trình đó cấu trúc dữ liệu chính chính xác. Và cũng giống như cây thuộc tính, đây là cơ hội tốt để đảm bảo rằng sau khi hoàn tất giai đoạn vẽ, đầu ra của giai đoạn này sẽ không thể thay đổi đối với 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 lợi ích to lớn về lâu dài. Thậm chí còn có nhiều lợi ích hơn bạn nghĩ:
- Cải thiện đáng kể độ tin cậy: Điều này khá đơn giản. Mã 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à dễ kiểm thử hơn. Điều này giúp hệ thống đáng tin cậy hơn. Điều này cũng giúp mã an toàn và ổn định hơn, ít sự cố hơn và ít lỗi use-after-free hơn.
- Mở rộng phạm vi kiểm thử: Trong quá trình phát triển BlinkNG, chúng tôi đã thêm rất nhiều chương trình kiểm thử mới vào bộ công cụ của mình. Điều này bao gồm các kiểm thử đơn vị cung cấp tính năng xác minh tập trung vào nội bộ; kiểm thử hồi quy giúp chúng ta không tái hiện các lỗi cũ đã được khắc phục (quá nhiều!); và nhiều nội dung bổ sung cho Bộ kiểm thử nền tảng web công khai, được duy trì tập thể. Tất cả trình duyệt đều sử dụng bộ kiểm thử này để đ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 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ỳ cấp độ chi tiết nào để tiến hành trên 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 giúp dễ dàng lý giải 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 điều lớn hơn như cuộn và ảnh động theo luồng chung 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 ta cải thiện hiệu suất một cách đáng kể, nhưng cũng cực kỳ phức tạp.
- Trả về và chứa: BlinkNG có một số tính năng mới giúp thực thi quy trình theo những cách mới và độc đáo. Ví dụ: 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 thì sao? Hay bỏ qua việ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 ngay lúc này? Đó là những gì thuộc tính CSS content-visibility 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à cụm từ tìm kiếm 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 trên nền tảng web (đây là tính năng được yêu cầu nhiều nhất từ các nhà phát triển CSS trong nhiều năm). Nếu nó tuyệt vời đến vậy, tại sao 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 bạn phải hiểu và kiểm soát rất cẩn thận mối quan hệ giữa mã 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 phần tử cấp trên. Vì kích thước bố cục được tính toán trong quá trình bố cục, nên chúng ta cần chạy tính toán lại kiểu sau bố cục; như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 truy vấn vùng chứa trước BlinkNG.
Làm cách nào để giải quyết vấn đề này? Đây 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ư Composite After Paint đã giải quyết không? Tệ hơn nữa, nếu các kiểu mới thay đổi kích thước của thành phần mẹ thì sao? Đôi khi điều này có dẫn đến vòng lặp vô hạn không?
Về nguyên tắc, bạn 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 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 mà 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ì các truy vấn vùng chứa yêu cầu chứa.
Nhưng thực sự, điều đó là chưa đủ và cần phải giới thiệu một loại vùng chứa yếu hơn so với vùng chứa kích thước. Điều này là do thường thì bạn muốn 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à khối) dựa trên kích thước nội tuyến của vùng chứa đó. Vì vậy, chúng tôi đã thêm khái niệm khả nă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, chúng tôi không hề rõ liệu có thể chứa kích thước nội tuyến hay không.
Mô tả tính năng chứa trong ngôn ngữ quy cách trừu tượng là một chuyện, còn triển khai tính năng này một cách chính xác lại là chuyện 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 chứa vào các lượt duyệt cây tạo nên logic chính của quá trình kết xuất: khi duyệt một cây con, không cần thông tin nào từ bên ngoài cây con. Khi điều này xảy ra (vâng, không hẳn là một tai nạn), việc triển khai tính năng chứa CSS sẽ nhiều gọn gàng và dễ dàng hơn nếu mã kết xuất tuân thủ nguyên tắc chứa.
Tương lai: kết hợp ngoài luồng chính … và nhiều tính năng khác!
Quy trình kết xuất hiển thị tại đây thực sự đi trước một chút so với cách triển khai RenderingNG hiện tại. Nó cho thấy việc phân lớp đang ở ngoài luồng chính, trong khi hiện tại, nó vẫn đang ở trên luồng chính. Tuy nhiên, việc này chỉ là vấn đề thời gian trước khi hoàn tất, giờ đây, Composite After Paint đã được xuất xưởng và quá trình phân lớp là sau khi vẽ.
Để hiểu lý do tại sao điều này quan trọng và những nơi khác có thể dẫn đến, chúng ta cần xem xét cấu trúc của công cụ kết xuất từ một góc độ cao hơn một chút. Một trong những trở ngại dai dẳng nhất để cải thiện hiệu suất của Chromium là thực tế đơn giản là luồng chính của trình kết xuất xử lý cả logic ứng dụng chính (tức là tập lệnh đang chạy) và phần lớn quá trình kết xuất. Do đó, luồng chính thường bị quá tải công việc và tình trạng 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 cần phải làm như vậy! Khía cạnh này của cấu trúc Chromium bắt nguồn từ thời KHTML, khi việc thực thi đơn luồng là mô hình lập trình chính. Khi bộ xử lý đa nhân trở nên phổ biến trong các thiết bị cấp tiêu dùng, giả định về luồng đơn đã được tích hợp triệt để vào Blink (trước đây là WebKit). Chúng tôi đã muốn giới thiệu thêm luồng vào công cụ kết xuất từ lâu, nhưng điều này đơn giản là không thể trong hệ thống cũ. Một trong những mục tiêu chính của Rendering NG là thoát khỏi hố này và cho phép di chuyển công việc kết xuất, một phần hoặc toàn bộ, sang một luồng (hoặc các luồng) khác.
Giờ đây, khi BlinkNG sắp hoàn tất, chúng tôi đã bắt đầu khám phá lĩnh vực này; Giao dịch không chặn là bước đầu tiên trong việc thay đổi mô hình tạo luồng của trình kết xuất. Giao dịch xác nhận của trình kết hợp (hoặc chỉ giao dịch xác nhận) là một bước đồng bộ hoá giữa luồng chính và luồng trình kết hợp. Trong quá trình xác nhận, 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 để mã kết hợp hạ nguồn chạy trên luồng trình kết hợp sử dụng. Trong khi quá trình đồng bộ hoá này diễn ra, quá trình thực thi luồng chính sẽ bị dừng trong khi mã sao chép chạy trên luồng trình kết hợp. Việc này được thực hiện để đả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 kết hợp đang sao chép dữ liệu đó.
Tính năng Gửi không chặn sẽ giúp luồng chính không cần phải dừng và chờ giai đoạn gửi kết thúc. Luồng chính sẽ tiếp tục thực hiện công việc trong khi quá trình gửi chạy đồng thời trên luồng trình kết hợp. Hiệu quả ròng của tính năng 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, giúp 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 đang hoạt động của tính năng Cam kết không chặn và đang chuẩn bị phân tích chi tiết về tác động của tính năng này đối với hiệu suất.
Chờ đợi trong cánh là Tạo ảnh tổng hợp ngoài luồng chính, với mục tiêu giúp 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à chuyển sang luồng worker. Giống như tính năng 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. 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ề cấu trúc của Composite After Paint.
Và chúng tôi còn nhiều dự án khác đang trong quá trình triển khai (dùng từ chơi chữ)! Cuối cùng, chúng tôi đã có một nền tảng cho phép 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 hào hứng để xem điều gì có thể xảy ra!