Tôi là Ian Kilpatrick, trưởng nhóm kỹ thuật của nhóm bố cục Blink cùng với Koji Ishii. Trước khi làm việc trong nhóm Blink, tôi là một kỹ sư phụ trách giao diện người dùng (trước khi Google có vai trò "kỹ sư phụ trách giao diện người dùng"), xây dựng các tính năng trong Google Tài liệu, Drive và Gmail. Sau khoảng 5 năm làm việc ở vị trí đó, tôi đã mạo hiểm chuyển sang nhóm Blink, học C++ một cách hiệu quả trong công việc và cố gắng tăng cường cơ sở mã Blink cực kỳ phức tạp. Ngay cả ngày nay, tôi cũng chỉ hiểu được một phần tương đối nhỏ trong số đó. Tôi rất cảm ơn bạn đã dành thời gian cho tôi trong khoảng thời gian này. Tôi cảm thấy an ủi khi biết rằng có rất nhiều "kỹ sư phụ trách giao diện người dùng đang hồi phục" đã chuyển đổi sang làm "kỹ sư trình duyệt" trước tôi.
Kinh nghiệm trước đây đã giúp tôi định hướng trong thời gian làm việc tại nhóm Blink. Là một kỹ sư phụ trách giao diện người dùng, tôi liên tục gặp phải các vấn đề về tính không nhất quán của trình duyệt, hiệu suất, lỗi hiển thị và thiếu tính năng. LayoutNG là cơ hội để tôi giúp khắc phục có hệ thống những vấn đề này trong hệ thống bố cục của Blink, đồng thời thể hiện tổng hợp nỗ lực của nhiều kỹ sư trong nhiều năm qua.
Trong bài đăng này, tôi sẽ giải thích cách một thay đổi lớn về cấu trúc như vậy có thể giảm thiểu và giảm bớt nhiều loại lỗi cũng như vấn đề về hiệu suất.
Cấu trúc công cụ bố cục ở tầm nhìn 30.000 feet
Trước đây, cây bố cục của Blink là "cây có thể thay đổi".
Mỗi đối tượng trong cây bố cục chứa thông tin đầu vào, chẳng hạn như kích thước hiện có do phần tử mẹ áp đặt, vị trí của mọi float và thông tin đầu ra, chẳng hạn như chiều rộng và chiều cao cuối cùng của đối tượng hoặc vị trí x và y của đối tượng.
Các đối tượng này được giữ lại giữa các lần kết xuất. Khi có thay đổi về kiểu, chúng ta đã đánh dấu đối tượng đó là không sạch và tương tự như tất cả các đối tượng mẹ trong cây. Khi chạy giai đoạn bố cục của quy trình kết xuất, chúng ta sẽ dọn dẹp cây, đi qua mọi đối tượng bẩn, sau đó chạy bố cục để đưa các đối tượng đó về trạng thái sạch.
Chúng tôi nhận thấy cấu trúc này dẫn đến nhiều loại vấn đề, mà chúng tôi sẽ mô tả bên dưới. Nhưng trước tiên, hãy lùi lại một chút và xem xét dữ liệu đầu vào và đầu ra của bố cục.
Về mặt khái niệm, việc chạy bố cục trên một nút trong cây này sẽ lấy "Kiểu cộng với DOM" và mọi quy tắc ràng buộc mẹ từ hệ thống bố cục mẹ (lưới, khối hoặc flex), chạy thuật toán ràng buộc bố cục và tạo ra kết quả.
Cấu trúc mới của chúng tôi chính thức hoá mô hình khái niệm này. Chúng ta vẫn có cây bố cục, nhưng chủ yếu sử dụng cây này để lưu giữ dữ liệu đầu vào và đầu ra của bố cục. Đối với đầu ra, chúng ta tạo một đối tượng không thể thay đổi hoàn toàn mới có tên là cây mảnh.
Tôi đã đề cập đến cây mảnh không thể thay đổi trước đó, mô tả cách cây này được thiết kế để sử dụng lại các phần lớn của cây trước đó cho các bố cục gia tăng.
Ngoài ra, chúng ta lưu trữ đối tượng ràng buộc mẹ đã tạo mảnh đó. Chúng ta sử dụng giá trị này làm khoá bộ nhớ đệm. Chúng ta sẽ thảo luận thêm về khoá này ở phần bên dưới.
Thuật toán bố cục nội tuyến (văn bản) cũng được viết lại để phù hợp với cấu trúc không thể thay đổi mới. Không chỉ tạo ra biểu diễn danh sách phẳng không thể thay đổi cho bố cục nội tuyến, mà còn có tính năng lưu vào bộ nhớ đệm cấp đoạn văn bản để bố cục lại nhanh hơn, hình dạng cho mỗi đoạn văn bản để áp dụng các tính năng phông chữ trên các phần tử và từ, thuật toán Unicode hai chiều mới sử dụng ICU, nhiều bản sửa lỗi chính xác và nhiều tính năng khác.
Các loại lỗi bố cục
Nói chung, lỗi bố cục thuộc 4 danh mục khác nhau, mỗi danh mục có nguyên nhân gốc khác nhau.
Tính chính xác
Khi nghĩ về lỗi trong hệ thống kết xuất, chúng ta thường nghĩ về tính chính xác, ví dụ: "Trình duyệt A có hành vi X, trong khi trình duyệt B có hành vi Y" hoặc "Cả trình duyệt A và B đều bị lỗi". Trước đây, đây là điều chúng tôi đã dành nhiều thời gian để thực hiện, và trong quá trình đó, chúng tôi liên tục phải đấu tranh với hệ thống. Một lỗi thường gặp là áp dụng một bản sửa lỗi rất cụ thể cho một lỗi, nhưng sau vài tuần, chúng tôi phát hiện ra rằng chúng tôi đã gây ra sự hồi quy trong một phần khác (có vẻ như không liên quan) của hệ thống.
Như mô tả trong các bài đăng trước, đây là dấu hiệu của một hệ thống rất dễ bị lỗi. Cụ thể đối với bố cục, chúng tôi không có hợp đồng rõ ràng giữa các lớp, khiến các kỹ sư trình duyệt phụ thuộc vào trạng thái mà họ không nên phụ thuộc hoặc diễn giải sai một số giá trị từ một phần khác của hệ thống.
Ví dụ: tại một thời điểm, chúng tôi có một chuỗi khoảng 10 lỗi trong hơn một năm, liên quan đến bố cục flex. Mỗi bản sửa lỗi đều gây ra vấn đề về độ chính xác hoặc hiệu suất trong một phần của hệ thống, dẫn đến một lỗi khác.
Giờ đây, LayoutNG xác định rõ ràng hợp đồng giữa tất cả các thành phần trong hệ thống bố cục, chúng ta nhận thấy rằng chúng ta có thể áp dụng các thay đổi một cách tự tin hơn nhiều. Chúng tôi cũng hưởng lợi rất nhiều từ dự án Kiểm thử nền tảng web (WPT) tuyệt vời, cho phép nhiều bên đóng góp vào một bộ kiểm thử web chung.
Hôm nay, chúng tôi nhận thấy rằng nếu phát hành một bản hồi quy thực sự trên kênh ổn định, thì bản phát hành đó thường không có kiểm thử liên quan trong kho lưu trữ WPT và không phải là do hiểu lầm về hợp đồng thành phần. Ngoài ra, theo chính sách khắc phục lỗi, chúng tôi luôn thêm một bài kiểm thử WPT mới để đảm bảo không có trình duyệt nào mắc lại lỗi tương tự.
Đang vô hiệu hoá
Nếu từng gặp phải lỗi bí ẩn mà việc đổi kích thước cửa sổ trình duyệt hoặc bật/tắt một thuộc tính CSS lại khiến lỗi biến mất một cách kỳ diệu, thì bạn đã gặp phải vấn đề về việc vô hiệu hoá không đầy đủ. Một phần của cây có thể thay đổi được coi là sạch, nhưng do một số thay đổi trong các điều kiện ràng buộc của phần tử mẹ, nên phần này không thể hiện kết quả chính xác.
Điều này rất phổ biến với các chế độ bố cục hai lượt (đi qua cây bố cục hai lần để xác định trạng thái bố cục cuối cùng) được mô tả bên dưới. Trước đây, mã của chúng ta sẽ có dạng như sau:
if (/* some very complicated statement */) {
child->ForceLayout();
}
Cách khắc phục loại lỗi này thường là:
if (/* some very complicated statement */ ||
/* another very complicated statement */) {
child->ForceLayout();
}
Việc khắc phục loại vấn đề này thường sẽ gây ra sự suy giảm hiệu suất nghiêm trọng (xem phần vô hiệu hoá quá mức bên dưới) và rất khó khắc phục.
Hôm nay (như mô tả ở trên), chúng ta có một đối tượng ràng buộc mẹ không thể thay đổi mô tả tất cả dữ liệu đầu vào từ bố cục mẹ đến bố cục con. Chúng ta lưu trữ dữ liệu này bằng mảnh không thể thay đổi thu được. Do đó, chúng ta có một nơi tập trung để diff hai dữ liệu đầu vào này nhằm xác định xem thành phần con có cần thực hiện một lượt truyền bố cục khác hay không. Logic so sánh này phức tạp nhưng được chứa gọn gàng. Việc gỡ lỗi cho loại vấn đề không hợp lệ này thường dẫn đến việc kiểm tra hai đầu vào theo cách thủ công và quyết định nội dung nào trong đầu vào đã thay đổi để yêu cầu một lượt truyền bố cục khác.
Các bản sửa lỗi cho mã so sánh này thường đơn giản và dễ dàng kiểm thử đơn vị do tính đơn giản của việc tạo các đối tượng độc lập này.
Mã so sánh cho ví dụ trên là:
if (width.IsPercent()) {
if (old_constraints.WidthPercentageSize()
!= new_constraints.WidthPercentageSize())
return kNeedsLayout;
}
if (height.IsPercent()) {
if (old_constraints.HeightPercentageSize()
!= new_constraints.HeightPercentageSize())
return kNeedsLayout;
}
Hiệu ứng trễ
Loại lỗi này tương tự như lỗi không vô hiệu hoá. Về cơ bản, trong hệ thống trước, rất khó để đảm bảo bố cục đó là idempotent – tức là bố cục chạy lại với cùng một dữ liệu đầu vào sẽ cho ra cùng một kết quả.
Trong ví dụ bên dưới, chúng ta chỉ cần chuyển đổi một thuộc tính CSS qua lại giữa hai giá trị. Tuy nhiên, điều này dẫn đến một hình chữ nhật "không ngừng phát triển".
Với cây có thể thay đổi trước đó, rất dễ gặp phải các lỗi như thế này. Nếu mã đọc sai kích thước hoặc vị trí của một đối tượng tại thời điểm hoặc giai đoạn không chính xác (ví dụ: chúng ta không "xoá" kích thước hoặc vị trí trước đó), thì chúng ta sẽ ngay lập tức thêm một lỗi hồi quy tinh vi. Những lỗi này thường không xuất hiện trong quá trình kiểm thử vì phần lớn các bài kiểm thử tập trung vào một bố cục và kết xuất duy nhất. Đáng lo ngại hơn nữa, chúng tôi biết rằng một số độ trễ này là cần thiết để một số chế độ bố cục hoạt động đúng cách. Chúng tôi gặp lỗi khi thực hiện tối ưu hoá để xoá một lượt truyền bố cục, nhưng lại gây ra "lỗi" vì chế độ bố cục yêu cầu hai lượt truyền để có được kết quả chính xác.
Với LayoutNG, vì chúng ta có cấu trúc dữ liệu đầu vào và đầu ra rõ ràng, đồng thời không cho phép truy cập vào trạng thái trước đó, nên chúng ta đã giảm thiểu đáng kể loại lỗi này khỏi hệ thống bố cục.
Hiệu suất và việc vô hiệu hoá quá mức
Đây là trường hợp đối lập trực tiếp với lớp lỗi không hợp lệ. Thông thường, khi khắc phục lỗi không hợp lệ, chúng ta sẽ kích hoạt hiệu suất giảm mạnh.
Chúng tôi thường phải đưa ra những lựa chọn khó khăn, ưu tiên độ chính xác hơn hiệu suất. Trong phần tiếp theo, chúng ta sẽ tìm hiểu sâu hơn về cách giảm thiểu các loại vấn đề về hiệu suất này.
Sự gia tăng của bố cục hai lượt và hiệu suất giảm mạnh
Bố cục Flex và lưới thể hiện sự thay đổi trong khả năng biểu đạt của bố cục trên web. Tuy nhiên, các thuật toán này về cơ bản khác với thuật toán bố cục khối trước đó.
Bố cục khối (trong hầu hết các trường hợp) chỉ yêu cầu công cụ thực hiện bố cục trên tất cả các thành phần con chính xác một lần. Điều này rất tốt cho hiệu suất, nhưng cuối cùng lại không thể hiện được như nhà phát triển web mong muốn.
Ví dụ: thường thì bạn muốn kích thước của tất cả các phần tử con mở rộng thành kích thước của phần tử lớn nhất. Để hỗ trợ việc này, bố cục mẹ (flex hoặc lưới) sẽ thực hiện một lượt đo lường để xác định kích thước của từng phần tử con, sau đó một lượt truyền bố cục sẽ kéo giãn tất cả phần tử con thành kích thước này. Hành vi này là mặc định cho cả bố cục flex và lưới.
Ban đầu, các bố cục hai lượt này có hiệu suất chấp nhận được, vì mọi người thường không lồng ghép sâu các bố cục này. Tuy nhiên, chúng tôi bắt đầu thấy các vấn đề nghiêm trọng về hiệu suất khi nội dung phức tạp hơn xuất hiện. Nếu bạn không lưu kết quả của giai đoạn đo lường vào bộ nhớ đệm, thì cây bố cục sẽ bị treo giữa trạng thái đo lường và trạng thái bố cục cuối cùng.
Trước đây, chúng ta đã cố gắng thêm các bộ nhớ đệm rất cụ thể vào bố cục flex và lưới để chống lại loại hiệu suất này. Cách này đã hoạt động (và chúng tôi đã tiến rất xa với Flex), nhưng liên tục phải chiến đấu với các lỗi vô hiệu hoá dưới và trên.
LayoutNG cho phép chúng ta tạo cấu trúc dữ liệu rõ ràng cho cả dữ liệu đầu vào và đầu ra của bố cục, ngoài ra, chúng tôi đã tạo bộ nhớ đệm của các lượt đo lường và bố cục. Điều này đưa độ phức tạp trở lại O(n), dẫn đến hiệu suất tuyến tính có thể dự đoán được cho các nhà phát triển web. Nếu có trường hợp bố cục đang thực hiện bố cục ba lượt, chúng ta cũng sẽ lưu lượt đó vào bộ nhớ đệm. Điều này có thể mở ra cơ hội để giới thiệu các chế độ bố cục nâng cao hơn một cách an toàn trong tương lai. Đây là ví dụ về cách RenderingNG về cơ bản mở khoá khả năng mở rộng trên toàn bộ. Trong một số trường hợp, bố cục Lưới có thể yêu cầu bố cục ba lượt, nhưng hiện tại trường hợp này rất hiếm.
Chúng tôi nhận thấy rằng khi nhà phát triển gặp phải các vấn đề về hiệu suất cụ thể với bố cục, thì điều này thường là do lỗi thời gian bố cục theo cấp số mũ thay vì thông lượng thô của giai đoạn bố cục trong quy trình. Nếu một thay đổi nhỏ (một phần tử thay đổi một thuộc tính css) dẫn đến bố cục 50-100 mili giây, thì đây có thể là lỗi bố cục theo cấp số nhân.
Tóm tắt
Bố cục là một lĩnh vực phức tạp, và chúng tôi không đề cập đến tất cả các loại chi tiết thú vị như tối ưu hoá bố cục nội tuyến (thực sự là cách toàn bộ hệ thống con nội tuyến và văn bản hoạt động), và ngay cả các khái niệm được đề cập ở đây thực sự chỉ là bề nổi và bỏ qua nhiều chi tiết. Tuy nhiên, hy vọng chúng tôi đã cho thấy việc cải thiện có hệ thống kiến trúc của một hệ thống có thể mang lại lợi ích vượt trội về lâu dài.
Tuy nhiên, chúng tôi biết rằng vẫn còn nhiều việc phải làm. Chúng tôi nhận thấy có một số vấn đề (cả về hiệu suất và độ chính xác) mà chúng tôi đang nỗ lực giải quyết, đồng thời rất hào hứng với các tính năng bố cục mới sắp ra mắt trên CSS. Chúng tôi tin rằng cấu trúc của LayoutNG giúp giải quyết các vấn đề này một cách an toàn và dễ dàng.
Một hình ảnh (bạn biết hình ảnh đó!) của Una Kravets.