
Đ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/
Ở ví dụ này chúng ta có 2 loại đối tượng chính:
- các icon màu xanh chạy lên xuống liên tục với tốc độ khác nhau
- cụm nút điều khiển gồm các chức năng như chèn thêm các icon màu xanh, xóa bớt icon màu xanh, dừng/tiếp tục chuyển động các icon, optimize (làm mượt) chuyển động cho các icons.
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 Optimize
và Un-Optimize
, đó là cách để lấy ra khoảng cách từ icon đến phần tử cha:
- nhánh
Un-Optimize
sẽ sử dụngelement.offsetTop
- nhánh
Optimize
sẽ sử dụngelement.style.top
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.
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.
Cách đọc thông số
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ụ Performance là Timeline 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
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.
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:
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:
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:
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 đỏ:
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 Optimize
và Un-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