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

Mariko Kosaka

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ìnhluồ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.

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

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.

DOM
Hình 2: Luồng chính phân tích cú pháp HTML và tạo cây DOM

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.

Kiểu đã tính toán
Hình 3: Luồng chính phân tích cú pháp CSS để thêm kiểu được 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 đượ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.

trò chơi máy fax của con người
Hình 4: Một người đứng trước một 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 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.

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

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

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

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.

không thành công với chỉ mục z
Hình 8: Các phần tử trang xuất hiện theo thứ tự của mã đánh dấu HTML, dẫn đến hình ảnh hiển thị không chính xác vì không tính đến chỉ mục z

Ở 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.

bản ghi paint
Hình 9: Luồng chính duyệt qua cây bố cục và tạo bản ghi sơn

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

Hình 10: Cây DOM+Style, Layout và Paint theo thứ tự được tạo

Đ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".

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 đ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.

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 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.

yêu cầu khung ảnh động
Hình 13: Các đoạn JavaScript nhỏ hơn chạy trên dòng thời gian có khung ảnh động

Kết hợp

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

Hình 14: Ảnh động về quy trình quét điểm ảnh đơn giản

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ì

Hình 15: Ảnh động về quá trình kết hợp

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.

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

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.

đường quét
Hình 17: Các luồng quét tạo bitmap của thẻ thông tin và gửi đến 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.

tổng hợp
Hình 18: Luồng trình kết hợp tạo khung kết hợp. Khung được gửi đến quy trình trình duyệt, sau đó đế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 moveclick 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.

Tiếp theo: Dữ liệu đầu vào sẽ được gửi đến trình kết hợp