Đo lường rendering performance sử dụng Chrome dev tools

Đo lường rendering performance sử dụng Chrome dev tools

60fps là mục tiêu cuối cùng của một web page "mượt"

Ở các bài viết trước, chúng ta đã tìm hiểu về quá trình rendering trên trình duyệt (rendering pipeline) cũng như các yếu tố ảnh hưởng đến rendering trên trình duyệt. Ở bài viết này, chúng ta sẽ học cách đánh giá rendering/runtime performance bằng công cụ.

Đánh giá định lượng

Để đánh giá rendering/runtime performance, thường chúng ta sẽ làm thế nào? Trước tiên dễ dàng nhất là nhìn và cảm nhận. Chúng ta có thể nói "trang web này mượt đó", hay "trang web này cứ giật giật, đơ đơ". Cách đánh giá này gọi là định tính, và nói về định tính nói chung thì chúng ta sẽ cần thu thập kết quả đánh giá ở phạm vi rộng và cần thời gian dài. Nguy hiểm hơn, đôi khi chúng ta có thể ko nhìn ra một web page đang có performance kém. Ví dụ bạn là dev "có điều kiện", bạn sử dụng cỗ máy i9-9900X, RTX 2080Ti, 16GB RAM DDR4 2666MHz, SSD Samsung Pro để nhìn và cảm nhận website bạn vừa làm. Bạn tự thấy mượt đó. Nhưng thiết bị của người dùng cuối render website bạn lại là những cỗ máy già cỗi trong các trường học sử dụng CPU Celeron hay Pentium với lượng RAM ít ỏi và đọc ghi chậm chạp, chắc chắn sẽ ko thể có trải nghiệm mượt giống như bạn đang trải nghiệm được.

Để rút ngắn quá trình đánh giá và tăng độ chính xác trong việc đánh giá rendering/runtime performance, chúng ta cần một công cụ giúp định lượng. Trong bài viết này chúng ta sẽ tìm hiểu về công cụ Performance trong bộ công cụ Chrome DevTools. Trước kia công cụ này có tên gọi là Timeline, tuy nhiên từ phiên bản Chrome 58 nó đã được đổi tên thành Performance.

Web page dùng để thực hành

Bài viết này được tham khảo từ bài viết Get Started With Analyzing Runtime Performance trên Google Developers website. Ở bài viết gốc, tác giả bài viết đã tạo ra một ví dụ rất phù hợp cho việc làm quen với công cụ Performance. Do đó, chúng ta cũng sẽ sử dụng luôn công ví dụ trong bài viết này để thực hành với công cụ Performance.

Chúng ta cùng mở web page ví dụ này trên trình duyệt Chrome: https://googlechrome.github.io/devtools-samples/jank/

Janky Animation - Google Chrome 2019-10-21 20.36.22.png

Ở ví dụ này chúng ta có 2 loại đối tượng chính:

Trước khi thực hành, chúng ta sẽ cùng review source code xem ví dụ này được cấu thành từ những gì. Source code gồm 3 files các bạn có thể xem tại đây. https://github.com/GoogleChrome/devtools-samples/tree/master/jank

Trong file HTML, ngoài cụm nút điều khiển, chúng ta sẽ có icon màu xanh với class name mover. Khi ấn nút Add 10, sẽ có thêm 10 icons với class name mover được thêm vào body.

<!doctype html>
<html>
  <head>
    <link rel="shortcut icon" href="/devtools-samples/favicon-96x96.png"/>
    <title>Janky Animation</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <script src="app.js" async></script>
  </head>
  <body>
    <img class="proto mover" src="../network/gs/logo-1024px.png"/>
    <div class="controls">
      <button class="add"></button>
      <button class="subtract" disabled></button>
      <button class="stop">Stop</button>
      <button class="optimize">Optimize</button>
      <a href="https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/"
         target="_blank">
        <button class="optimize">Help</button>
      </a>
    </div>
  </body>
</html>

Trong file css chúng ta sẽ thấy được styling chính cần quan tâm đó là styling của các icon mover:

/* ... */

.mover {
  height: 3vw;
  position: absolute;
  z-index: 0;
}

/* ... */

Cuối cùng là app.js.

(function(window) {
  'use strict';

  var app = {},
      proto = document.querySelector('.proto'),
      movers,
      bodySize = document.body.getBoundingClientRect(),
      ballSize = proto.getBoundingClientRect(),
      maxHeight = Math.floor(bodySize.height - ballSize.height),
      maxWidth = 97, // 100vw - width of square (3vw)
      incrementor = 10,
      distance = 3,
      frame,
      minimum = 10,
      subtract = document.querySelector('.subtract'),
      add = document.querySelector('.add');

  app.optimize = false;
  app.count = minimum;
  app.enableApp = true;

  app.init = function () {
    if (movers) {
      bodySize = document.body.getBoundingClientRect();
      for (var i = 0; i < movers.length; i++) {
        document.body.removeChild(movers[i]);
      }
      document.body.appendChild(proto);
      ballSize = proto.getBoundingClientRect();
      document.body.removeChild(proto);
      maxHeight = Math.floor(bodySize.height - ballSize.height);
    }
    for (var i = 0; i < app.count; i++) {
      var m = proto.cloneNode();
      var top = Math.floor(Math.random() * (maxHeight));
      if (top === maxHeight) {
        m.classList.add('up');
      } else {
        m.classList.add('down');
      }
      m.style.left = (i / (app.count / maxWidth)) + 'vw';
      m.style.top = top + 'px';
      document.body.appendChild(m);
    }
    movers = document.querySelectorAll('.mover');
  };

  app.update = function (timestamp) {
    for (var i = 0; i < app.count; i++) {
      var m = movers[i];
      if (!app.optimize) {
        var pos = m.classList.contains('down') ?
            m.offsetTop + distance : m.offsetTop - distance;
        if (pos < 0) pos = 0;
        if (pos > maxHeight) pos = maxHeight;
        m.style.top = pos + 'px';
        if (m.offsetTop === 0) {
          m.classList.remove('up');
          m.classList.add('down');
        }
        if (m.offsetTop === maxHeight) {
          m.classList.remove('down');
          m.classList.add('up');
        }
      } else {
        var pos = parseInt(m.style.top.slice(0, m.style.top.indexOf('px')));
        m.classList.contains('down') ? pos += distance : pos -= distance;
        if (pos < 0) pos = 0;
        if (pos > maxHeight) pos = maxHeight;
        m.style.top = pos + 'px';
        if (pos === 0) {
          m.classList.remove('up');
          m.classList.add('down');
        }
        if (pos === maxHeight) {
          m.classList.remove('down');
          m.classList.add('up');
        }
      }
    }
    frame = window.requestAnimationFrame(app.update);
  }

  document.querySelector('.stop').addEventListener('click', function (e) {
    if (app.enableApp) {
      cancelAnimationFrame(frame);
      e.target.textContent = 'Start';
      app.enableApp = false;
    } else {
      frame = window.requestAnimationFrame(app.update);
      e.target.textContent = 'Stop';
      app.enableApp = true;
    }
  });

  document.querySelector('.optimize').addEventListener('click', function (e) {
    if (e.target.textContent === 'Optimize') {
      app.optimize = true;
      e.target.textContent = 'Un-Optimize';
    } else {
      app.optimize = false;
      e.target.textContent = 'Optimize';
    }
  });

  add.addEventListener('click', function (e) {
    cancelAnimationFrame(frame);
    app.count += incrementor;
    subtract.disabled = false;
    app.init();
    frame = requestAnimationFrame(app.update);
  });

  subtract.addEventListener('click', function () {
    cancelAnimationFrame(frame);
    app.count -= incrementor;
    app.init();
    frame = requestAnimationFrame(app.update);
    if (app.count === minimum) {
      subtract.disabled = true;
    }
  });

  function debounce(func, wait, immediate) {
    var timeout;
    return function() {
      var context = this, args = arguments;
      var later = function() {
        timeout = null;
        if (!immediate) func.apply(context, args);
      };
      var callNow = immediate && !timeout;
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
      if (callNow) func.apply(context, args);
    };
  };

  var onResize = debounce(function () {
    if (app.enableApp) {
        cancelAnimationFrame(frame);
        app.init();
        frame = requestAnimationFrame(app.update);
    }
  }, 500);

  window.addEventListener('resize', onResize);

  add.textContent = 'Add ' + incrementor;
  subtract.textContent = 'Subtract ' + incrementor;
  document.body.removeChild(proto);
  proto.classList.remove('.proto');
  app.init();
  window.app = app;
  frame = window.requestAnimationFrame(app.update);

})(window);

Trong file này, chỗ đáng chú ý nhất đó là hàm update(). Đây chính là hàm được sử dụng để tạo ra chuyển động cho các icons màu xanh. Hàm này sẽ được gọi thông qua method window.requestAnimationFrame() mà chúng ta đã biết trong các bài viết trước. Bên trong hàm này, các bạn có thể nhìn thấy câu lệnh rẽ nhánh if (!app.optimize) { /* ... */ } tương ứng với nút điều khiển Un-Optimize và rẽ nhánh else tương đương lệnh điều khiển Optimize trên giao diện.

Dự đoán hung thủ

Điểm khác biệt lớn nhất giữa tùy chọn OptimizeUn-Optimize, đó là cách để lấy ra khoảng cách từ icon đến phần tử cha:

Như các bạn (có thể) đã biết, element.style.top có tốc độ thực thi cao hơn element.offsetTop. Nếu bạn hỏi tại sao thì xin phép chúng ta sẽ bàn trong một dịp khác.

Do đó, chúng ta có thể dự đoán, nghi phạm có thể gây ra vấn đề hiệu năng là các dòng code sử dụng phương thức element.offsetTop(). Giờ chúng ta sẽ điều tra dựa vào các bằng chứng xem thực sự có như dự đoán không.

Tiến hành đo lường bằng công cụ Performance

Mục tiêu đầu tiên

Quay lại ví dụ trên, mục tiêu đầu tiên của chúng ta đó là làm sao cho web page trở nên "cà giật" hoặc "lag", sau đó chúng ta sẽ dùng công cụ Performance để tìm ra nguyên nhân.

Tiếp đó trên giao diện web, các bạn click vào button Add 10 đến khi nào cảm thấy các icon chuyển động giật dần.

Tiếp theo chúng ta mở Chrome DevTools (Command+Option+I đối với Mac hoặc Control+Shift+I đối với Windows, Linux cho bạn nào chưa biết), chọn tab Performance. Để nhanh nhìn thấy hiệu quả hơn, chúng ta có thể giả lập làm chậm tốc độ CPU bằng cách bấm vào icon bánh răng và chỉnh CPU throttle.

Janky Animation - Google Chrome 2019-10-21 21.20.19.png

Các bạn chọn mức độ làm chậm 2x, 4x, 8x tùy vào độ khỏe chiếc máy tính mà các bạn đang sử dụng.

Khi bạn thấy các icon chuyển động đủ "giật lag", chúng ta sẽ bắt đầu đo lường.

Khi đã sẵn sàng, các bạn bấm vào nút Record. Chờ một vài giây (trong ví dụ của mình là 10s), sau đó bấm Stop.

Chờ tiếp một vài giây, chúng ta sẽ có kết quả đo lường vài giây runtime vừa qua một cách vô cùng chi tiết.

DevTools - googlechrome.github.io_devtools-samples_jank_ 2019-10-21 22.19.29.png

Cách đọc thông số

dfc1c636-002b-4799-8a6e-b87d9260766e.png

Bảng kết quả trả về này là một biểu đồ 2 trục, trong đó trục X chính là thời gian mà chúng ta record, trục này có bước nhảy rất nhỏ tính bằng ms (mili giây). Đây cũng chính là xuất phát điểm của tên gọi cũ của công cụ PerformanceTimeline do dữ liệu được biểu thị theo dòng chảy thời gian.

Ở trục Y, chúng ta lần lượt có các thông số:

FPS CPU NET

ab9a294c-4ba0-45e5-8f79-54713eb00f25.png

Khu vực này chúng ta có thể có cái nhìn tổng quan về vấn đề về hiệu năng dựa biểu đồ biểu thị FPS (Frames per second) và CPU usage qua thời gian.

Frames

Khu vực vực này sẽ cho chúng ta thông số chi tiết về số khung hình qua thời gian. Bạn có thể thử hover vào một ô màu xanh lá cây ở khu vực này. Một ô tương ứng với một quãng thời gian thực thi.

77332f58-cc1d-4d9a-a17a-e90fca7ffe2e.png

Như ở ví dụ này của mình, quãng thời gian thực thi này kéo dài 102ms, 1 giây có 1000ms, do đó nếu thực thi mất 102ms thì trong 1 giây đó quá trình thực thi chỉ thực hiện được 1000/102 ~ 10 lần, tương ứng ~10fps. Như chúng ta đã cùng tìm hiểu ở các bài viết trước, mục tiêu về FPS luôn phải là 60fps, tương ứng 16.67ms cho mỗi lần thực thi.

Main

Đây là khu vực chính để chúng ta tìm ra thủ phạm. Nơi đây sẽ biểu thị các sự kiện được trigger trong Main thread, trong đây sẽ gồm nhiều sự kiện, có thể xuất phát từ Scripting, Rendering, Painting (đều là các sự kiện trong Rendering pipeline)

Bắt đầu tìm thủ phạm

Nhìn vào tab Summary chúng ta có thể ngay lập tức khoanh vùng được nguyên nhân:

3471212b-95a3-475b-b79d-d43c04abce95.png

Từ biểu đồ quạt (pie chart), chúng ta có thể nhìn ra 2 đối tượng có thời gian thực thi dài nhất, đó là Scripting (tương ứng màu cam) và Rendering (tương ứng màu tím).

Tiếp theo click chuột vào vùng FPS CPU NET, bạn sẽ thấy một khu vực nhỏ vừa click sáng lên so với vùng còn lại, lúc này chúng ta đang chọn một khoảng thời gian thí điểm trong quãng thời gian 10s để tiến hành điều tra. Click vào vùng Main(nếu vùng main chưa được mở sẵn), lúc này bạn sẽ nhìn thấy một thanh màu cam (tương ứng Scripting) có tên Animation Frame Fired với một kí hiệu tam giác đỏ ở góc bên phải. Đây chính là cảnh báo sự kiện này có thể đang gây ra vấn đề về performance. Tiếp tục click vào thanh này, lúc này tab Summary sẽ thay đổi với nội dung tương ứng:

905ae08a-8402-4119-9eb7-d25ea0a9ec3b.png

Trong tab summary, bạn có thể nhìn được thông tin chi tiết hơn về event này, cụ thể event này đc trigger từ Animation Frame Requested, rất có thể chính là phương thức requestAnimationFrame(), ở dòng 95 trong file app.js. Để kiểm nghiệm, các bạn có thể bấm vào link app.js:95. Dòng 95 sẽ xuất hiện với nội dung frame = window.requestAnimationFrame(app.update);, đúng như dự đoán. Từ dòng code 95, chúng ta cũng có thể suy ra ngay nguồn gốc tiếp tục xuất phát từ hàm update(). Để kiểm nghiệm, chúng ta lại nhìn ngược lên khu vực Main, ngay dưới event Animation Frame Fired , chúng ta có sự kiện Function call với thời gian dài tương đương, tiếp tục click vào sự kiện Funtion call và nhìn xuống tab Summary, chúng ta sẽ thấy kết quả kiểm nghiệm:

7412af77-1a02-423c-a05a-c9b194ad0fef.png

Kết quả là app:js dòng 62 app.update = function (timestamp) {.

Đến đây chúng ta mới tìm ra nguyên nhân ở mức độ hàm. Chúng ta vẫn cần khoang vùng nhỏ hơn nữa.

Vẫn song song trong khoảng thời gian đó, nhìn tiếp xuống dưới sự kiện Function call, chúng ta sẽ thấy 1 loạt sự kiện nhỏ li ti màu tìm tương đương với Rendering. Và đa số chúng có hình tam giác đỏ ở góc. Click vào 1 thằng màu tím bất kì có tam giác đỏ:

1de806c2-8748-412b-bc58-e939728dc3e5.png

Tiếp tục nhìn vào tab Summary, hung thủ đã hiện ra rõ ràng. Warning: Forced reflow is a likely performance bottleneck. => có gì đó trong khu vực này gây ra hiện tượng reflow. xuất phát từ app.js dòng 71. Và dòng 71 là:

if (m.offsetTop === 0) {

Vậy là hung thủ và nghi phạm trùng nhau.

Kết

Trên đây mình đã hướng dẫn một cách cơ bản cách tiếp cận công cụ Performance của Chrome DevTools. Đến đây mình nghĩ các bạn có thể tiếp tục thử với nhiều trường hợp khác ở ví dụ trên để làm quen với các đọc các thông số. Ví dụ record trường hợp Optimize, hoặc record cả 2 trường hợp OptimizeUn-Optimize cùng lúc. Để tìm hiểu cặn kẽ hơn về công cụ này, các bạn có thể truy cập trực tiếp ChromeDev Tools official doc tại https://developers.google.com/web/tools/chrome-devtools/evaluate-performance

Reference: https://developers.google.com/web/tools/chrome-devtools/evaluate-performance

Previous Post Next Post