Tìm hiểu về trình duyệt web hiện đại (phần 3)

Mariko Kosaka

Hoạt động bên trong của Quy trình kết xuất

Đây là phần 3 trong loạt blog gồm 4 phần về cách hoạt động của trình duyệt. Trước đây, chúng tôi đã đề cập đến cấu trúc đa quy trìnhluồng điều hướng. Trong bài đăng này, chúng ta sẽ xem những gì xảy ra bên trong quy trình kết xuất đồ hoạ.

Quy trình của trình kết xuất ảnh hưởng đến nhiều khía cạnh của hiệu suất web. Vì có rất nhiều sự kiện xảy ra bên trong quy trình kết xuất đồ hoạ nên bài đăng này chỉ là thông tin tổng quan chung. Nếu bạn muốn tìm hiểu sâu hơn, mục Hiệu suất của khoá học Kiến thức cơ bản về web sẽ có nhiều tài nguyên khác.

Quy trình trình kết xuất xử lý nội dung trên web

Quá trình kết xuất chịu trách nhiệm về mọi hoạt động diễn ra bên trong một thẻ. Trong quy trình kết xuất đồ hoạ, luồng chính xử lý hầu hết mã mà bạn gửi cho người dùng. Đôi khi, một số phần trong JavaScript của bạn sẽ được các luồng worker xử lý nếu bạn sử dụng một trình chạy web hoặc một trình chạy dịch vụ. Các chuỗi trình tổng hợp và đường quét cũng được chạy bên trong các quy trình kết xuất đồ hoạ để kết xuất một trang một cách hiệu quả và mượt mà.

Nhiệm vụ cốt lõi của quy trình kết xuất đồ hoạ là biến HTML, CSS và JavaScript thành một trang web mà người dùng có thể tương tác.

Quy trình của trình kết xuất
Hình 1: Quy trình trình kết xuất có luồng chính, luồng worker, luồng trình tổng hợp và luồng đường quét bên trong

Phân tích cú pháp

Xây dựng DOM

Khi quá trình kết xuất nhận được thông báo xác nhận cho một hoạt động điều hướng và bắt đầu nhận dữ liệu HTML, luồng chính sẽ bắt đầu phân tích cú pháp chuỗi văn bản (HTML) và biến chuỗi đó thành một Dô Oodel Model (DOM).

DOM là đại diện nội bộ của trang web cũng như cấu trúc dữ liệu và API mà nhà phát triển web có thể tương tác thông qua JavaScript.

Việc phân tích cú pháp một tài liệu HTML thành một DOM được xác định theo Tiêu chuẩn HTML. Bạn có thể nhận thấy rằng việc cấp HTML cho trình duyệt không bao giờ gửi lỗi. Ví dụ: thiếu thẻ đóng </p> là một HTML hợp lệ. Mã đánh dấu lỗi như Hi! <b>I'm <i>Chrome</b>!</i> (thẻ b đóng trước thẻ i) được xử lý như thể bạn đã viết Hi! <b>I'm <i>Chrome</i></b><i>!</i>. Điều này là do thông số kỹ thuật HTML được thiết kế để xử lý các lỗi đó một cách linh hoạt. Nếu muốn biết những việc này được thực hiện như thế nào, bạn có thể đọc phần "Giới thiệu về cách xử lý lỗi và các trường hợp lạ trong trình phân tích cú pháp" của thông số kỹ thuật HTML.

Đang tải tài nguyên phụ

Một trang web thường sử dụng các tài nguyên bên ngoài như hình ảnh, CSS và JavaScript. Các tệp đó cần được tải từ mạng hoặc bộ nhớ đệm. Luồng chính có thể yêu cầu từng phần tử một khi chúng tìm thấy chúng trong khi phân tích cú pháp để xây dựng DOM, nhưng để tăng tốc, tính năng "tải trước trình quét" sẽ được chạy đồng thời. Nếu có các nội dung như <img> hoặc <link> trong tài liệu HTML, hãy tải trước trình quét sẽ xem nhanh các mã thông báo do trình phân tích cú pháp HTML tạo và gửi yêu cầu tới chuỗi mạng trong quá trình xử lý của trình duyệt.

Mô hình đối tượng tài liệu (DOM)
Hình 2: Phân tích cú pháp HTML và xây dựng cây DOM

JavaScript có thể chặn quá trình phân tích cú pháp

Khi tìm thấy thẻ <script>, trình phân tích cú pháp HTML sẽ tạm dừng việc phân tích cú pháp tài liệu HTML đồng thời phải tải, phân tích cú pháp và thực thi mã JavaScript. Tại sao? bởi vì JavaScript có thể thay đổi hình dạng của tài liệu bằng cách sử dụng những thứ như document.write() thay đổi toàn bộ cấu trúc DOM (thông tin tổng quan về mô hình phân tích cú pháp trong thông số HTML có một sơ đồ đẹp). Do đó, trình phân tích cú pháp HTML phải đợi JavaScript chạy trước khi có thể tiếp tục phân tích cú pháp tài liệu HTML. Nếu bạn tò mò về điều gì xảy ra trong quá trình thực thi JavaScript thì nhóm V8 đã trò chuyện và đăng bài trên blog về vấn đề này.

Gợi ý cho trình duyệt về cách bạn muốn tải tài nguyên

Có nhiều cách để nhà phát triển web có thể gửi gợi ý đến trình duyệt để tải tài nguyên hiệu quả. Nếu JavaScript của bạn không sử dụng document.write(), bạn có thể thêm thuộc tính async hoặc defer vào thẻ <script>. Sau đó, trình duyệt tải và chạy mã JavaScript một cách không đồng bộ và không chặn quá trình phân tích cú pháp. Bạn cũng có thể sử dụng mô-đun JavaScript nếu thích hợp. <link rel="preload"> là một cách để thông báo cho trình duyệt biết tài nguyên này chắc chắn cần thiết cho hoạt động điều hướng hiện tại và bạn muốn tải xuống càng sớm càng tốt. Bạn có thể đọc thêm về vấn đề này trong phần Ưu tiên tài nguyên – Tải trình duyệt để giúp bạn.

Tính toán kiểu

Chỉ có DOM là chưa đủ để biết trang trông như thế nào vì chúng tôi có thể tạo kiểu cho các phần tử trang trong CSS. Luồng chính phân tích cú pháp CSS và xác định kiểu được tính toán cho mỗi nút DOM. Đây là thông tin về loại kiểu được áp dụng cho từng phần tử dựa trên bộ chọn CSS. Bạn có thể xem thông tin này trong phần computed của Công cụ cho nhà phát triển.

Kiểu điện toán
Hình 3: Phân tích cú pháp CSS của luồng chính để thêm kiểu đã tính toán

Ngay cả khi bạn không cung cấp CSS nào, mỗi nút DOM đều có một kiểu đã tính toán. Thẻ <h1> hiển thị lớn hơn thẻ <h2> và lề được xác định cho từng phần tử. Nguyên nhân là do trình duyệt có một biểu định kiểu mặc định. Nếu muốn biết CSS mặc định của Chrome là như thế nào, bạn có thể xem mã nguồn tại đây.

Bố cục

Bây giờ, quá trình kết xuất đã biết cấu trúc của một tài liệu và kiểu cho mỗi nút, nhưng như vậy là không đủ để kết xuất một trang. Hãy tưởng tượng bạn đang cố gắng mô tả một bức tranh cho bạn bè qua chiếc điện thoại. "Có một hình tròn màu đỏ lớn và một hình vuông nhỏ màu xanh dương" là không đủ thông tin để bạn bè của bạn biết chính xác bức tranh trông như thế nào.

trò chơi về máy fax của con người
Hình 4: Một người đang đứng trước bức tranh, đường dây điện thoại kết nối với người khác

Bố cục là một quá trình tìm hình dạng của các phần tử. Luồng chính đi qua DOM và các kiểu được tính toán, đồng thời tạo cây bố cục có thông tin như toạ độ x y và kích thước hộp giới hạn. Cây bố cục có thể có cấu trúc tương tự như cây DOM, nhưng chỉ chứa thông tin liên quan đến nội dung hiển thị trên trang. Nếu bạn áp dụng display: none, thì phần tử đó sẽ không thuộc cây bố cục (tuy nhiên, phần tử có visibility: hidden sẽ nằm trong cây bố cục). Tương tự, nếu áp dụng một phần tử giả có nội dung như p::before{content:"Hi!"}, thì phần tử đó sẽ được đưa vào cây bố cục ngay cả khi phần tử đó không có trong DOM.

bố cục
Hình 5: Luồng chính vượt qua cây DOM với các kiểu đã tính toán và tạo ra cây bố cục
Hình 6: Bố cục hộp cho một đoạn văn do sự thay đổi về ngắt dòng

Xác định Bố cục của một trang là một nhiệm vụ khó khăn. Ngay cả bố cục trang đơn giản nhất như một luồng khối từ trên xuống dưới cũng phải xem xét độ lớn của phông chữ và vị trí ngắt dòng vì những yếu tố đó ảnh hưởng đến kích thước và hình dạng của một đoạn văn; điều này sau đó ảnh hưởng đến vị trí của đoạn sau.

CSS có thể làm cho phần tử nổi sang một bên, che mục bị tràn và thay đổi hướng viết. Bạn có thể tưởng tượng giai đoạn bố cục này có một nhiệm vụ rất lớn. Trong Chrome, cả một đội ngũ kỹ sư sẽ phụ trách bố cục. Nếu bạn muốn xem thông tin chi tiết về công việc của họ, thì một số bài nói chuyện từ BlinkOn hội nghị sẽ được ghi lại và rất thú vị để xem.

Sơn

trò chơi vẽ tranh
Hình 7: Một người cầm bút vẽ trước bức tranh vẽ, tự hỏi liệu họ nên vẽ hình tròn trước hay hình vuông trước

Có DOM, kiểu và bố cục vẫn là không đủ để hiển thị trang. Giả sử bạn đang cố gắng tái tạo một bức tranh. Bạn biết kích thước, hình dạng và vị trí của các phần tử, nhưng bạn vẫn phải đánh giá thứ tự tô các phần tử đó.

Ví dụ: z-index có thể được đặt cho một số phần tử nhất định. Trong trường hợp đó, việc vẽ theo thứ tự của các phần tử được viết trong HTML sẽ dẫn đến kết xuất không chính xác.

chỉ mục z không thành công
Hình 8: Các phần tử trang xuất hiện theo thứ tự đánh dấu HTML, dẫn đến hình ảnh kết xuất không chính xác vì chỉ mục z không được xem xét

Ở bước vẽ này, luồng chính sẽ đi bộ trên cây bố cục để tạo bản ghi vẽ. Bản ghi vẽ là một lưu ý về quy trình tô màu, chẳng hạn như "nền trước rồi đến văn bản rồi đến hình chữ nhật". Nếu bạn đã vẽ trên phần tử <canvas> bằng JavaScript, thì quy trình này có thể quen thuộc với bạn.

sao chép bản ghi
Hình 9: Luồng chính đi qua cây bố cục và tạo bản ghi vẽ

Việc cập nhật quy trình kết xuất tốn kém

Hình 10: DOM+Kiểu, Bố cục và Sơn cây theo thứ tự được tạo

Điều quan trọng nhất cần nắm được trong quy trình kết xuất là ở mỗi bước, kết quả của thao tác trước đó sẽ được dùng để tạo dữ liệu mới. Ví dụ: nếu có nội dung thay đổi trong cây bố cục, thì bạn cần tạo lại thứ tự Paint cho các phần bị ảnh hưởng của tài liệu.

Nếu bạn đang tạo ảnh động cho các phần tử, trình duyệt phải chạy các thao tác này ở giữa mọi khung hình. Hầu hết các màn hình của chúng tôi làm mới màn hình 60 lần/giây (60 khung hình/giây); ảnh động sẽ trông mượt mà trước mắt người khi bạn di chuyển đối tượng trên màn hình ở mọi khung hình. Tuy nhiên, nếu ảnh động bỏ lỡ các khung hình ở giữa, thì trang sẽ xuất hiện hiện tượng "giật".

hiện tượng giật do thiếu khung hình
Hình 11: Khung ảnh động trên dòng thời gian

Ngay cả khi các hoạt động kết xuất của bạn phù hợp với quá trình làm mới màn hình, các phép tính này vẫn đang chạy trên luồng chính. Điều này có nghĩa là các phép tính này có thể bị chặn khi ứng dụng của bạn đang chạy JavaScript.

hiện tượng giật do JavaScript
Hình 12: Các khung ảnh động trên dòng thời gian, nhưng một khung bị JavaScript chặn

Bạn có thể chia hoạt động JavaScript thành các phần nhỏ và lên lịch chạy ở mọi khung hình bằng cách sử dụng requestAnimationFrame(). Để biết thêm thông tin về chủ đề này, vui lòng xem phần Tối ưu hoá quá trình thực thi JavaScript. Bạn cũng có thể chạy JavaScript trong Web Workers để tránh chặn luồng chính.

yêu cầu khung ảnh động
Hình 13: Các phần JavaScript nhỏ hơn chạy trên tiến trình có khung ảnh động

Đang tổng hợp

Bạn sẽ vẽ một trang như thế nào?

Hình 14: Ảnh động của quy trình tạo điểm ảnh ban đầu

Bây giờ, trình duyệt đã biết cấu trúc của tài liệu, kiểu của từng phần tử, hình học của trang và thứ tự hiển thị, trình duyệt sẽ vẽ trang như thế nào? Việc chuyển thông tin này thành các pixel trên màn hình được gọi là tạo điểm ảnh.

Có lẽ cách đơn giản để xử lý vấn đề này là thực hiện các phần đường quét bên trong khung nhìn. Nếu người dùng cuộn trang, thì hãy di chuyển khung đã quét và lấp đầy các phần còn thiếu bằng cách tạo điểm ảnh thêm. Đây là cách Chrome xử lý quá trình tạo điểm ảnh khi phát hành lần đầu tiên. Tuy nhiên, trình duyệt hiện đại chạy một quy trình phức tạp hơn gọi là tổng hợp.

Kết hợp là gì

Hình 15: Ảnh động của quy trình kết hợp

Tổng hợp là một kỹ thuật tách các phần của một trang thành các lớp, tạo điểm ảnh cho các phần đó riêng biệt và tổng hợp dưới dạng một trang trong một chuỗi riêng biệt được gọi là luồng trình tổng hợp. Nếu xảy ra thao tác cuộn, vì các lớp đã được tạo điểm ảnh, nên tất cả những gì cần làm chỉ là kết hợp một khung mới. Bạn có thể tạo ảnh động theo cách tương tự bằng cách di chuyển các lớp và kết hợp một khung mới.

Bạn có thể xem cách trang web của mình được chia thành các lớp trong Công cụ cho nhà phát triển bằng cách sử dụng bảng điều khiển Lớp.

Chia thành các lớp

Để tìm ra phần tử cần chứa lớp nào, luồng chính sẽ đi qua cây bố cục để tạo cây lớp (phần này được gọi là "Cập nhật cây lớp" trong bảng điều khiển hiệu suất của Công cụ cho nhà phát triển). Nếu một số phần của trang nên là lớp riêng biệt (như trình đơn bên dạng trượt trong) không nhận được một lớp, thì bạn có thể gợi ý cho trình duyệt bằng cách sử dụng thuộc tính will-change trong CSS.

cây phân lớp
Hình 16: Luồng chính đi qua cây bố cục tạo cây lớp

Bạn có thể muốn cung cấp các lớp cho mọi phần tử, nhưng việc kết hợp trên quá nhiều lớp có thể dẫn đến hoạt động chậm hơn so với tạo điểm ảnh cho các phần nhỏ của một trang trong mỗi khung hình. Vì vậy, bạn cần phải đo lường hiệu suất kết xuất của ứng dụng. Để biết thêm thông tin về chủ đề này, hãy xem bài viết Gắn với thuộc tính chỉ dành cho trình tổng hợp và Quản lý số lượng lớp.

Đường quét và tổ hợp ngoài luồng chính

Sau khi tạo cây lớp và xác định thứ tự vẽ, luồng chính sẽ cam kết thông tin đó vào luồng của trình tổng hợp. Sau đó, luồng trình tổng hợp sẽ tạo điểm ảnh cho từng lớp. Một lớp có thể lớn bằng toàn bộ chiều dài của một trang, vì vậy, luồng của trình tổng hợp sẽ chia chúng thành các thẻ thông tin và gửi từng thẻ thông tin đến các chuỗi đường quét. Các luồng đường quét sẽ tạo điểm ảnh cho từng thẻ thông tin và lưu trữ chúng trong bộ nhớ GPU.

đường quét
Hình 17: Các luồng đường quét tạo bitmap của thẻ thông tin và gửi tới GPU

Luồng trình tổng hợp có thể ưu tiên các luồng đường quét khác nhau để những thứ trong khung nhìn (hoặc lân cận) có thể được quét trước. Một lớp cũng có nhiều ô xếp kề cho các độ phân giải khác nhau để xử lý những thao tác như phóng to.

Sau khi thẻ thông tin được tạo điểm ảnh, luồng tổng hợp sẽ thu thập thông tin thẻ thông tin có tên là tứ phân vị trí vẽ để tạo khung trình tổng hợp.

Vẽ các ô Chứa các thông tin như vị trí của thẻ thông tin trong bộ nhớ và vị trí trên trang để vẽ thẻ thông tin có tính đến quá trình kết hợp trang.
Khung của bộ tổng hợp Tập hợp các góc vẽ đại diện cho một khung của một trang.

Sau đó, khung trình tổng hợp được gửi tới quy trình của trình duyệt qua IPC. Tại thời điểm này, bạn có thể thêm một khung trình tổng hợp khác từ luồng giao diện người dùng để thay đổi giao diện người dùng của trình duyệt hoặc từ các quy trình kết xuất khác cho tiện ích. Những khung hình tổng hợp này được gửi đến GPU để hiển thị trên màn hình. Nếu một sự kiện cuộn xuất hiện, luồng trình tổng hợp sẽ tạo một khung trình tổng hợp khác để gửi đến GPU.

tổng hợp
Hình 18: Luồng tổng hợp đang tạo khung kết hợp. Khung hình sẽ được gửi đến tiến trình của trình duyệt sau đó đến GPU

Lợi ích của việc kết hợp là được thực hiện mà không liên quan đến luồng chính. Luồng trình tổng hợp không cần chờ tính toán kiểu hoặc thực thi JavaScript. Đây là lý do tại sao chỉ kết hợp ảnh động được xem là tốt nhất để có hiệu suất mượt mà. Nếu cần tính toán lại bố cục hoặc lớp vẽ, thì luồng chính phải được tham gia.

Tổng kết

Trong bài đăng này, chúng ta đã xem xét quy trình kết xuất từ phân tích cú pháp đến kết hợp. Hy vọng rằng giờ đây bạn có đủ thông tin để đọc thêm về cách tối ưu hoá hiệu suất của trang web.

Trong bài đăng tiếp theo cũng như bài đăng cuối cùng của loạt bài này, chúng ta sẽ tìm hiểu chi tiết hơn về luồng trình tổng hợp và xem điều gì sẽ xảy ra khi người dùng nhập mouse moveclick.

Bạn có thích bài đăng này không? Nếu bạn có câu hỏi hoặc đề xuất cho bài đăng trong tương lai, tôi muốn lắng nghe bạn trong phần bình luận bên dưới hoặc gửi @kosamari trên Twitter.

Tiếp theo: Dữ liệu đầu vào sắp được chuyển đến trình tổng hợp