Cách Javascript hoạt động P11: Render engine & mẹo tối ưu hóa hiệu năng render

November 25, 2018 (5y ago)

Chào các bạn đến với bài thứ 11 trong series đục khoét và khám phá Javascript cũng như các thành phần của nó. Trong quá trình xác định và tìm hiểu các thành phần cốt lõi, tác giả cũng chia sẻ một số nguyên tắc mà họ đang dùng để xây dựng SessionStack, một ứng dụng Javascript hướng đến sự mạnh mẽ, hiệu năng cao và ổn định.

Trong những bài trước của series "Đục khoét Javascript", chúng ta đã tập trung vào tìm hiểu ngôn ngữ Javascript, các tính năng của nó, cách chúng được thực thi trên trình duyệt, làm thế nào để tối ưu hóa, vân vân.

Tuy nhiên, khi bạn xây dựng webapp, bạn không chỉ viết code Javascript. Code của bạn còn tương tác với môi trường. Thấu hiểu môi trường, cách nó hoạt động cũng như các thành phần của nó sẽ cho phép bạn xây dựng app tốt hơn và có nền tảng chuẩn bị tốt để đề phòng những nguy cơ tiềm tàng có thể xảy đến bất cứ lúc nào khi lên production.

Các thành phần chính của trình duyệt:

Trong bài này, chúng ta sẽ tập trung vào rendering engine (engine dựng hình), bởi vì nó xử lý quá trình phân giải và hình ảnh hóa (visualization) code HTML & CSS, là phần mà đa số app Javascript cần tương tác liên tục.

Khái quát về rendering engine

Công việc chính của rendering engine là hiển thị trang được yêu cầu lên màn hình của trình duyệt.

Rendering engine có thể hiển thị HTML, văn bản XML và ảnh. Nếu bạn sử dụng thêm plugin ở ngoài thì engine có thể hiển thị các loại văn bản khác, chẳng hạn như PDF.

Rendering engines

Tương tự như Javascript engine, trình duyệt khác nhau cũng sử dụng các rendering engine khác nhau. Một vài bộ engine nổi tiếng:

Quá trình render

Rendering engine nhận nội dung của văn bản được yêu cầu từ lớp networking.

Xây dựng DOM tree

Bước đầu tiên của công cuộc rendering là phân giải văn bản HTML và chuyển những phần tử đã phân giải thành những DOM node thực sự trong DOM tree.

Giả sử bạn có đoạn input như sau:

<html>
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" type="text/css" href="theme.css" />
  </head>
  <body>
    <p>Hello, <span> friend! </span></p>
    <div>
      <img src="smiley.gif" alt="Smiley face" height="42" width="42" />
    </div>
  </body>
</html>

DOM tree của đoạn HTML trên sẽ giống như sau:

Về cơ bản thì mỗi phần tử được thể hiện như là một node cha của tất cả các element khác nằm trực tiếp ngay bên dưới (bên trong) nó. Nguyên tắc này được áp dụng một cách đệ quy.

Xây dựng CSSOM tree

CSSOM viết tắt của CSS Object Model. Trong khi trình duyệt đang xây dựng DOM, nó bắt gặp một thẻ link trong phần head và dẫn tới một file CSS tên là theme.css ở bên ngoài. Dự đoán rằng nó có thể cần đến tài nguyên này để render trang, ngay lập tức nó điều phối 1 request đến. Giả sử file theme.css có nội dung như sau:

body {
  font-size: 16px;
}

p {
  font-weight: bold;
}

span {
  color: red;
}

p span {
  display: none;
}

img {
  float: right;
}

Tương tự HTML, engine cần chuyển tất cả CSS sang một thứ gì đó mà trình duyệt có thể xử lý, chính là CSSOM. Dưới đây là mô phỏng của CSSOM tree:

Bạn có tự hỏi tại sao CSSOM lại có cấu trúc dạng cây (tree)? Khi tính toán bộ style cuối cùng cho mỗi object tren trang, trình duyệt sẽ bắt đầu với rule áp dụng toàn cục nhất cho node đó (ví dụ: nếu nó là con của phần tử body thì áp dụng tất cả style của body) và tinh chỉnh một cách đệ quy những style đã được tính toán bằng cách áp dụng các rule cụ thể hơn.

Với ví dụ ở trên, bất kỳ text nào nằm bên trong thẻ span mà span nằm trong phần tử body thì đều có font-size 16 và màu đỏ. Những style này được kế thừa từ phần tử body. Nếu như span là con của phần tử p thì nội dung của nó sẽ bị ẩn bởi vì có style khác cụ thể hơn đã được áp dụng cho nó (ở đây là display: none).

Thêm nữa, lưu ý rằng tree ở trên chưa phải là CSSOM tree hoàn chỉnh và chỉ thể hiện những style mà ta đã ghi đè trong style sheet. Mỗi trình duyệt cung cấp 1 bộ style mặc định, còn được biết tới là user agent styles - đây chính những gì ta thấy nếu như không cung cấp style cụ thể. Style của chúng ta thêm vào chỉ đơn giản là ghi đè lại những phần mặc định này.

Xây dựng render tree

Cùng với phần thể hiện trực quan trong HTML kết hợp với dữ liệu style từ CSSOM tree là chúng ta đã có đủ nguyên liệu để tạo ra render tree.

Bạn sẽ thắc mắc "render tree" là gì? Nó là 1 cây (tree) của các phần tử trực quan được xây dựng theo thứ tự trong đó chúng được hiển thị trên màn hình. Đó là sự thể hiện 1 cách trực quan của HTML cùng với CSS tương ứng. Mục đích của cây này là cho phép tô màu nội dung theo đúng thứ tự.

Mỗi node trong render tree được gọi là 1 renderer hoặc render object trong Webkit.

Dưới đây là cách mà render tree của DOM & CSSOM ở trên thể hiện:

Để xây dựng render tree, trình duyệt về cơ bản sẽ làm những bước sau đây:

Bạn có thể xem qua source code của RenderObject (WebKit) ở đây: https://github.com/WebKit/webkit/blob/fde57e46b1f8d7dde4b2006aaf7ebe5a09a6984b/Source/WebCore/rendering/RenderObject.h

Cùng nghía qua một vài dòng cốt lõi trong class này nhé:

class RenderObject : public CachedImageClient {
  // Tô màu lại toàn bộ object. Nó sẽ được gọi khi border color thay đổi hoặc
  // border style thay đổi.

  Node* node() const { ... }

  RenderStyle* style;  // the computed style
  const RenderStyle& style() const;

  ...
}

Mỗi renderer thể hiện một khu vực hình chữ nhật tương ứng với CSS box của một node. Nó bao gồm cả thông tin hình học như độ rộng (width), chiều cao (height) hay vị trí (position).

Cách bố trí của render tree

Khi renderer được tạo ra và thêm vào tree, nó không có thông tin vị trí hay kích thước, phần tính toán các giá trị này được gọi là layout.

HTML sử dụng mô hình layout theo dòng (flow-based layout), nghĩa là hầu như toàn bộ thời gian nó có thể tính toán thông số hình học chỉ trong 1 lần duyệt. Hệ thống tọa độ có liên quan đến root renderer. Thông số tọa độ top và left được sử dụng.

Layout là 1 quá trình đệ quy, nó bắt đầu ở root renderer, chính là thứ tương ứng với phần tử <html> trong văn bản HTML. Layout tiếp tục duyệt đệ quy qua một hoặc toàn bộ cây cấp bậc(hierarchy) renderer, tính toán các thông tin hình học cần thiết cho mỗi renderer.

Vị trí của root renderer là 0,0 và kích thước của nó bằng phần nhìn thấy được của cửa sổ hiển thị trên trình duyệt (còn gọi là viewport).

Bắt đầu quá trình tạo layout chính là truyền đạt lại cho mỗi node tọa độ chính xác mà nó cần phải xuất hiện trên màn hình là ở đâu.

Tô màu cho render tree

Trong giai đoạn này, renderer tree đã được duyệt qua và phương thức paint() của renderer được gọi để hiển thị nội dung lên màn hình.

Tô màu có thể theo cách global hoặc incremantal tương tự như layout):

Về tổng quát thì quan trọng là cần phải hiểu rằng tô màu là quá trình diễn ra từ từ. Để có UX tốt hơn, render engine sẽ cố hiển thị nội dung trên màn hình ngay khi có thể. Nó sẽ không ngồi yên đợi cho tới khi toàn bộ HTML được parse để bắt đầu xây dựng và bố trí render tree. Từng phần của nội dung sẽ được parse và hiển thị lên trong khi tiến trình tiếp tục với những item nội dung tiếp theo đang được truyền về trên mạng.

Thứ tự xử lý script và style

Các script được parse và thực thi ngay lập tức khi parser vừa gặp thẻ <script>. Quá trình parse của toàn bộ văn bản sẽ tạm dừng cho đến khi script thực thi xong. Nghĩa là tiến trình này diễn ra đồng bộ.

Nếu như script là file ở ngoài thì việc đầu tiên nó cần phải được lấy về từ mạng (bất đồng bộ). Tất cả công việc parse sẽ dừng lại cho đến khi lấy xong file.

HTML5 có thêm 1 tùy chọn để đánh dấu script là bất đồng bộ, do đó nó có thể được parse và thực thi trong 1 tiến trình khác.

Tối ưu hóa hiệu suất render

Nếu bạn muốn tối ưu hóa app thì có 5 điểm chính mà bạn cần tập trung vào dưới đây:

  1. Javascript - trong các bài trước chúng ta đã nghiên cứu về chủ đề viết code tối ưu và có hiệu quả bộ nhớ cao mà không làm ảnh hưởng đến UI. Với trường hợp của render, chúng ta cần phải suy nghĩ về cách mà code Javascript sẽ tương tác với các phần tử DOM trên trang. Javascript có thể tạo ra rất nhiều thay đổi với UI, đặc biệt là các app SPA.
  2. Tính toán Style - đây là tiến trình xác định CSS rule nào sẽ áp dụng vào phần tử nào dựa trên các selector. Một khi các rule đã được định nghĩa, chúng sẽ được áp dụng và tính toán style cuối cùng cho mỗi phần tử.
  3. Layout - khi trình duyệt biết rule nào áp dụng cho phần tử nào, nó có thể bắt đầu tính toán bao nhiêu không gian một phần tử sẽ chiếm dụng và vị trí của nó sẽ nằm ở đâu trên màn hình của trình duyệt. Mô hình layout của trang web xác định một phần tử có thể gây ảnh hưởng đến phần tử khác. Ví dụ, độ rộng của <body> có thể ảnh hưởng độ rộng của phần tử con của nó. Điều này nghĩa là quá trình layout sẽ là quá trình nặng về tính toán số học. Phần "vẽ" được thực hiện trong nhiều layer khác nhau.
  4. Tô màu - đây là lúc mà các pixel thực sự được lên màu. Tiến trình bao gồm cả phần vẽ các câu chữ, màu sắc, hình ảnh, viền, đổ bóng, vấn vân, từng phần nhìn thấy được của từng phần tử.
  5. Kết hợp (Compositing) - Bởi vì các phần nhỏ của webpage được vẽ vào trong nhiều lớp khác nhau, chúng cần được kết hợp vào một màn hình theo đúng thứ tự để page có thể render một cách chính xác. Điều này rất quan trọng, đặc biệt là với các phần tử chồng nhau.

Tối ưu hóa JavaScript

Javascript thường trigger những thay đổi nhìn thấy được trên trình duyệt. Và những tác vụ đó nhân lên nhiều lần khi xây dựng ứng dụng SPA.

Dưới đây là 1 số mẹo nhỏ để bạn biết nên tối ưu phần nào của code Javascript nhằm cải thiện render:

Tối ưu hóa CSS

Chỉnh sửa DOM bằng cách thêm bớt các phần tử, thay đổi các thuộc tính... sẽ làm cho trình duyệt phải tính toán lại style của phần tử và trong nhiều trường hợp, là phải tính lại layout của toàn bộ trang hoặc 1 phần của trang.

Để tối ưu quá trình render, bạn cần cân nhắc những điều sau:

Tối ưu hóa layout

Tính toán lại layout có thể ngốn nhiều tài nguyên của trình duyệt nên bạn cần cân nhắc những điều sau:

Tối ưu hóa tô màu

Đây thường là tác vụ chạy lâu nhất trong số các tác vụ nên quan trọng là tránh mặt nó càng xa càng tốt. Những gì bạn có thể làm:

Render là một khía cạnh quan trọng trong cách thức hoạt động của SessionStack. SessionStack phải tái tạo lại một video về mọi thứ đã diễn ra với user tại thời điểm họ trải nghiệm qua một vấn đề khi đang lướt webapp của bạn. Để làm được điều này, SessionStack chỉ xử lý duy nhất những dữ liệu mà thư viện của nó thu thập được: các sự kiện từ user, thay đổi trên DOM, request lên mạng, biệt lệ, thông báo debug, vân vân. Trình phát video được tối ưu hóa tối đa để có thể render một cách chính xác và sử dụng toàn bộ những dữ liệu thu thập được để có thể đưa ra một bản giả lập trình duyệt của user hoàn-hảo-đến-từng-pixel cũng như những gì đã xảy ra trên đó, cả về mặt kỹ thuật lẫn quan sát.