Cách hoạt động bên trong của Quy trình kết xuất
Đây là phần 3 trong loạt bài gồm 4 phần trên blog, tìm hiểu cách hoạt động của trình duyệt. Trước đây, chúng ta đã đề cập đến cấu trúc đa quy trình và luồng điều hướng. Trong bài đăng này, chúng ta sẽ xem xét những gì xảy ra bên trong quy trình kết xuất.
Quy trình trình kết xuất ảnh chạm đến nhiều khía cạnh của hiệu suất web. Vì có rất nhiều hoạt động diễn ra bên trong quy trình kết xuất, 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, phần Hiệu suất của Kiến thức cơ bản về web có nhiều tài nguyên khác.
Quy trình trình kết xuất xử lý nội dung web
Quy trình kết xuất chịu trách nhiệm cho mọi hoạt động diễn ra bên trong một thẻ. Trong quy trình kết xuất, luồng chính xử lý hầu hết mã bạn gửi đến người dùng. Đôi khi, các phần của JavaScript được xử lý bằng luồng worker nếu bạn sử dụng worker web hoặc worker dịch vụ. Luồng kết hợp và luồng đường quét cũng chạy bên trong quy trình kết xuất để hiển thị trang một cách hiệu quả và trơn tru.
Công việc cốt lõi của quy trình kết xuất là biến HTML, CSS và JavaScript thành một trang web mà người dùng có thể tương tác.

Phân tích cú pháp
Xây dựng DOM
Khi quy trình trình kết xuất nhận được thông báo xác nhận cho một thao tác đ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ô Object Model (DOM) của Document.
DOM là nội dung trình bày nội bộ của trình duyệt về trang 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 tài liệu HTML thành DOM được xác định theo Tiêu chuẩn HTML. Bạn có thể nhận thấy rằng việc cung cấp HTML cho trình duyệt
không bao giờ gửi lỗi. Ví dụ: thiếu thẻ đóng </p>
là HTML hợp lệ. Mã đánh dấu không chính xác như Hi! <b>I'm <i>Chrome</b>!</i>
(thẻ b được đóng trước thẻ i) được coi như 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 tò mò về cách thực hiện những việc này, 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 quy cách HTML.
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. Bạn cần tải các tệp đó từ mạng hoặc bộ nhớ đệm. Luồng chính có thể yêu cầu từng tệp khi tìm thấy các tệp đó trong khi phân tích cú pháp để tạo DOM, nhưng để tăng tốc, "trình quét tải trước" sẽ chạy đồng thời.
Nếu có những nội dung như <img>
hoặc <link>
trong tài liệu HTML, trình quét tải trước sẽ xem trước 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 đến luồng mạng trong quy trình duyệt web.

JavaScript có thể chặn quá trình phân tích cú pháp
Khi trình phân tích cú pháp HTML tìm thấy thẻ <script>
, trình phân tích cú pháp sẽ tạm dừng việc phân tích cú pháp tài liệu HTML và phải tải, phân tích cú pháp và thực thi mã JavaScript. Tại sao? vì JavaScript có thể thay đổi hình dạng của tài liệu bằng cách sử dụng các thành phần như document.write()
, giúp thay đổi toàn bộ cấu trúc DOM (tổng quan về mô hình phân tích cú pháp trong thông số kỹ thuật HTML có một sơ đồ đẹp). Đây là lý 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ề những gì xảy ra trong quá trình thực thi JavaScript, nhóm V8 có các bài nói chuyện và bài đăng 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 nhằm tải tài nguyên một cách hiệu quả.
Nếu JavaScript 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 sẽ tải và chạy mã JavaScript 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 phù hợp. <link rel="preload">
là một cách để thông báo cho trình duyệt rằng 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 bài viết Ưu tiên tài nguyên – Yêu cầu trình duyệt trợ giúp bạn.
Tính toán kiểu
Chỉ có DOM là chưa đủ để biết trang sẽ trông như thế nào vì chúng ta 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 mục computed
của DevTools.

Ngay cả khi bạn không cung cấp CSS nào, mỗi nút DOM đều có một kiểu được 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ử. Điều này là do trình duyệt có một trang kiểu mặc định. Nếu muốn biết CSS mặc định của Chrome, bạn có thể xem mã nguồn tại đây.
Bố cục
Bây giờ, quy trình kết xuất biết cấu trúc của tài liệu và kiểu cho từng nút, nhưng điều đó chưa đủ để hiển thị 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 mình qua điện thoại. "Có một vòng tròn lớn màu đỏ và một hình vuông nhỏ màu xanh dương" là thông tin không đủ để bạn bè của bạn biết chính xác bức tranh sẽ trông như thế nào.

Bố cục là một quá trình tìm hình học của các phần tử. Luồng chính sẽ duyệt 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 bạn á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 mặc dù không có trong DOM.

Xác định Bố cục của một trang là một nhiệm vụ đầy thách thức. Ngay cả bố cục trang đơn giản nhất như luồng khối từ trên xuống dưới cũng phải xem xét kích thước phông chữ và vị trí ngắt dòng vì những yếu tố này ảnh hưởng đến kích thước và hình dạng của một đoạn văn bản; sau đó ảnh hưởng đến vị trí của đoạn văn bản tiếp theo.
CSS có thể làm cho phần tử nổi sang một bên, che mục 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ụ to lớn. Trong Chrome, có cả một nhóm kỹ sư làm việc trên bố cục. Nếu bạn muốn xem thông tin chi tiết về công việc của họ, có một số bài nói chuyện từ Hội nghị BlinkOn được ghi lại và khá thú vị để xem.
Sơn

Việc có DOM, kiểu và bố cục vẫn chưa đủ để hiển thị một trang. Giả sử bạn đang cố gắng tạo lại 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ự vẽ các phần tử đó.
Ví dụ: bạn có thể đặt z-index
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 quả hiển thị không chính xác.

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

Việc cập nhật quy trình kết xuất tốn kém
Điều quan trọng nhất cần nắm bắt trong quy trình kết xuất là ở mỗi bước, kết quả của thao tác trước đó được dùng để tạo dữ liệu mới. Ví dụ: nếu có gì 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 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ẽ xuất hiện mượt mà đối với mắt người khi bạn di chuyển các đối tượng trên màn hình ở mỗi khung hình. Tuy nhiên, nếu ảnh động bị thiếu các khung hình ở giữa, thì trang sẽ xuất hiện "gượng gạo".

Ngay cả khi các hoạt động kết xuất của bạn đang theo kịp tốc độ 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, tức là luồng này có thể bị chặn khi ứng dụng của bạn đang chạy JavaScript.

Bạn có thể chia thao tác 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 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 Worker để tránh chặn luồng chính.

Kết hợp
Bạn sẽ vẽ một trang như thế nào?
Giờ đây, 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ự vẽ, trình duyệt sẽ vẽ trang như thế nào? Việc chuyển đổi thông tin này thành pixel trên màn hình được gọi là quét đường quét.
Có lẽ cách đơn giản để xử lý vấn đề này là quét các phần bên trong khung nhìn. Nếu người dùng cuộn trang, hãy di chuyển khung đã quét và điền vào các phần còn thiếu bằng cách quét thêm. Đây là cách Chrome xử lý việc quét điểm ảnh khi được phát hành lần đầu. 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à kết hợp.
Tính năng kết hợp là gì
Kết hợp là một kỹ thuật để tách các phần của trang thành các lớp, quét riêng các lớp đó và kết hợp dưới dạng một trang trong một luồng riêng biệt có tên là luồng trình kết hợp. Nếu cuộn xảy ra, vì các lớp đã được quét đường quét, tất cả những gì bạn cần làm 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 hình mới.
Bạn có thể xem cách trang web của mình được chia thành các lớp trong DevTools bằng cách sử dụng Bảng điều khiển Layers (Lớp).
Phân chia thành các lớp
Để tìm ra phần tử nào cần nằm trong 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 DevTools). Nếu một số phần nhất định của trang phải là lớp riêng biệt (chẳng hạn như trình đơn bên trượt vào) không nhận được 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.

Bạn có thể muốn tạo lớp cho mọi phần tử, nhưng việc kết hợp trên một số lượng lớp quá mức có thể dẫn đến hoạt động chậm hơn so với việc tạo điểm ảnh cho các phần nhỏ của trang mỗi khung hình. Vì vậy, điều quan trọng là bạn phải đo lường hiệu suất kết xuất của ứng dụng. Để biết thêm về chủ đề này, hãy xem phần Chỉ sử dụng các thuộc tính dành cho trình kết hợp và quản lý số lượng lớp.
Kết xuất đường quét và tổng hợp ra khỏi luồng chính
Sau khi tạo cây lớp và xác định thứ tự sơn, luồng chính sẽ cam kết thông tin đó với luồng trình kết hợp. Sau đó, luồng trình kết hợp sẽ quét từng lớp. Một lớp có thể lớn như toàn bộ chiều dài của một trang, vì vậy, luồng trình kết hợp sẽ chia các lớp đó thành các ô và gửi từng ô đến các luồng đường quét. Luồng đường quét sẽ quét từng ô và lưu trữ các ô đó trong bộ nhớ GPU.

Luồng trình kết hợp có thể ưu tiên các luồng đường quét khác nhau để các đối tượng trong khung nhìn (hoặc gần đó) có thể được quét trước. Một lớp cũng có nhiều ô xếp kề cho nhiều độ phân giải để xử lý các thao tác như thu phóng.
Sau khi các thẻ thông tin được tạo điểm ảnh, luồng trình kết hợp sẽ thu thập thông tin về thẻ thông tin có tên là vẽ tứ giác để tạo khung trình kết hợp.
Vẽ hình tứ giác | Chứa 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 việc kết hợp trang. |
Khung trình kết hợp | Một tập hợp các hình vẽ tứ giác đại diện cho một khung của trang. |
Sau đó, một khung tổng hợp sẽ được gửi đến quy trình trình duyệt thông qua IPC. Tại thời điểm này, bạn có thể thêm một khung trình kết hợp khác từ luồng giao diện người dùng cho thay đổi giao diện người dùng của trình duyệt hoặc từ các quy trình trình kết xuất khác cho tiện ích. Các 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 có một sự kiện cuộn, luồng trình kết hợp sẽ tạo một khung trình kết hợp khác để gửi đến GPU.

Lợi ích của tính năng kết hợp là việc này được thực hiện mà không cần đến luồng chính. Luồng trình kết hợp không cần phải chờ tính toán kiểu hoặc thực thi JavaScript. Đó là lý do tại sao chỉ kết hợp ảnh động được coi là cách tốt nhất để đạt được hiệu suất mượt mà. Nếu cần tính toán lại bố cục hoặc sơn thì luồng chính phải được tham gia.
Kết thúc
Trong bài đăng này, chúng ta đã xem xét quy trình kết xuất từ việc phân tích cú pháp đến kết hợp. Hy vọng rằng giờ đây, bạn đã có thể đọ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 và cũng là bài đăng cuối cùng của loạt bài này, chúng ta sẽ xem xét chi tiết hơn về luồng trình kết hợp và xem điều gì sẽ xảy ra khi dữ liệu đầu vào của người dùng như mouse move
và click
xuất hiện.
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 rất mong nhận được ý kiến của bạn trong phần bình luận bên dưới hoặc @kosamari trên Twitter.