Ngày xuất bản: 31 tháng 3 năm 2014
Để xác định và giải quyết các nút thắt cổ chai về hiệu suất của đường dẫn kết xuất quan trọng, bạn cần có kiến thức tốt về các lỗi thường gặp. Hướng dẫn xác định các mẫu hiệu suất phổ biến sẽ giúp bạn tối ưu hoá các trang của mình.
Việc tối ưu hoá đường dẫn kết xuất quan trọng cho phép trình duyệt vẽ trang nhanh nhất có thể: các trang tải nhanh hơn sẽ giúp tăng mức độ tương tác, số trang được xem và tăng số lượt chuyển đổi. Để giảm thiểu thời gian khách truy cập xem màn hình trống, chúng ta cần tối ưu hoá tài nguyên nào được tải và thứ tự tải.
Để minh hoạ quy trình này, hãy bắt đầu với trường hợp đơn giản nhất có thể và từng bước xây dựng trang của chúng ta để thêm các tài nguyên, kiểu và logic ứng dụng. Trong quá trình này, chúng ta sẽ tối ưu hoá từng trường hợp; đồng thời, chúng ta cũng sẽ xem xét những điểm có thể xảy ra lỗi.
Cho đến nay, chúng ta chỉ tập trung vào những gì xảy ra trong trình duyệt sau khi có tài nguyên (tệp CSS, JS hoặc HTML) để xử lý. Chúng tôi đã bỏ qua thời gian cần thiết để tìm nạp tài nguyên từ bộ nhớ đệm hoặc từ mạng. Chúng ta sẽ giả định như sau:
- Một vòng truyền qua mạng (độ trễ truyền tải) đến máy chủ mất 100 mili giây.
- Thời gian phản hồi của máy chủ là 100 mili giây đối với tài liệu HTML và 10 mili giây đối với tất cả các tệp khác.
Trải nghiệm hello world
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Critical Path: No Style</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
Bắt đầu bằng mã đánh dấu HTML cơ bản và một hình ảnh; không có CSS hoặc JavaScript. Sau đó, hãy mở bảng điều khiển Mạng trong Công cụ của Chrome cho nhà phát triển và kiểm tra thác nước tài nguyên thu được:
Như dự kiến, tệp HTML mất khoảng 200 mili giây để tải xuống. Xin lưu ý rằng phần trong suốt của đường màu xanh dương thể hiện khoảng thời gian mà trình duyệt chờ trên mạng mà không nhận được bất kỳ byte phản hồi nào, trong khi phần màu đồng nhất cho biết thời gian để hoàn tất quá trình tải xuống sau khi nhận được các byte phản hồi đầu tiên. Tệp HTML tải xuống có kích thước rất nhỏ (<4K), vì vậy, tất cả những gì chúng ta cần là một lượt truy xuất dữ liệu để tìm nạp toàn bộ tệp. Do đó, tài liệu HTML mất khoảng 200 mili giây để tìm nạp, trong đó một nửa thời gian chờ mạng và nửa còn lại chờ phản hồi của máy chủ.
Khi nội dung HTML có sẵn, trình duyệt sẽ phân tích cú pháp các byte, chuyển đổi các byte đó thành mã thông báo và tạo cây DOM. Lưu ý rằng DevTools báo cáo thời gian cho sự kiện DOMContentLoaded ở dưới cùng (216 mili giây) một cách thuận tiện, thời gian này cũng tương ứng với đường dọc màu xanh dương. Khoảng thời gian giữa khi quá trình tải HTML kết thúc và đường kẻ dọc màu xanh dương (DOMContentLoaded) là thời gian trình duyệt cần để tạo cây DOM – trong trường hợp này, chỉ vài mili giây.
Lưu ý rằng "ảnh tuyệt vời" của chúng ta không chặn sự kiện domContentLoaded
. Hóa ra, chúng ta có thể tạo cây kết xuất và thậm chí vẽ trang mà không cần chờ từng thành phần trên trang: không phải tất cả tài nguyên đều quan trọng để phân phối lần vẽ đầu tiên nhanh. Trên thực tế, khi nói về đường dẫn kết xuất quan trọng, chúng ta thường nói về mã đánh dấu HTML, CSS và JavaScript. Hình ảnh không chặn quá trình kết xuất ban đầu của trang, mặc dù chúng ta cũng nên cố gắng vẽ hình ảnh càng sớm càng tốt.
Tuy nhiên, sự kiện load
(còn gọi là onload
) bị chặn trên hình ảnh: DevTools báo cáo sự kiện onload
ở 335 mili giây. Hãy nhớ rằng sự kiện onload
đánh dấu thời điểm tất cả tài nguyên mà trang yêu cầu đã được tải xuống và xử lý; tại thời điểm này, vòng quay tải có thể ngừng quay trong trình duyệt (đường dọc màu đỏ trong thác nước).
Thêm JavaScript và CSS vào
Trang "Trải nghiệm Hello World" có vẻ cơ bản, nhưng thực tế có rất nhiều điều diễn ra. Trong thực tế, chúng ta sẽ cần nhiều hơn là HTML: rất có thể, chúng ta sẽ có một trang kiểu CSS và một hoặc nhiều tập lệnh để thêm một số tính năng tương tác vào trang của mình. Thêm cả hai vào bản phối để xem điều gì sẽ xảy ra:
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Script</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body onload="measureCRP()">
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="timing.js"></script>
</body>
</html>
Trước khi thêm JavaScript và CSS:
Với JavaScript và CSS:
Việc thêm các tệp CSS và JavaScript bên ngoài sẽ thêm hai yêu cầu bổ sung vào thác nước của chúng ta, tất cả các yêu cầu này đều được trình duyệt gửi đi cùng một lúc. Tuy nhiên, hãy lưu ý rằng hiện tại, sự khác biệt về thời gian giữa sự kiện domContentLoaded
và onload
nhỏ hơn nhiều.
Điều gì đã xảy ra?
- Không giống như ví dụ HTML thuần tuý, chúng ta cũng cần tìm nạp và phân tích cú pháp tệp CSS để tạo CSSOM, đồng thời cần cả DOM và CSSOM để tạo cây kết xuất.
- Vì trang này cũng chứa một trình phân tích cú pháp chặn tệp JavaScript, nên sự kiện
domContentLoaded
bị chặn cho đến khi tệp CSS được tải xuống và phân tích cú pháp: vì JavaScript có thể truy vấn CSSOM, nên chúng ta phải chặn tệp CSS cho đến khi tệp này tải xuống thì mới có thể thực thi JavaScript.
Điều gì sẽ xảy ra nếu chúng ta thay thế tập lệnh bên ngoài bằng tập lệnh nội tuyến? Ngay cả khi tập lệnh được đưa trực tiếp vào trang, trình duyệt cũng không thể thực thi tập lệnh đó cho đến khi CSSOM được tạo. Tóm lại, JavaScript nội tuyến cũng là trình phân tích cú pháp chặn.
Tuy nhiên, mặc dù chặn trên CSS, việc nội tuyến tập lệnh có giúp trang hiển thị nhanh hơn không? Hãy thử và xem điều gì sẽ xảy ra.
JavaScript bên ngoài:
JavaScript nội tuyến:
Chúng ta sẽ gửi ít yêu cầu hơn, nhưng cả thời gian onload
và domContentLoaded
đều giống nhau. Tại sao? Chúng ta biết rằng không quan trọng JavaScript có nội tuyến hay bên ngoài, vì ngay khi trình duyệt gặp thẻ tập lệnh, trình duyệt sẽ chặn và đợi cho đến khi CSSOM được tạo. Hơn nữa, trong ví dụ đầu tiên, trình duyệt sẽ tải cả CSS và JavaScript song song và các tệp này sẽ tải xong gần như cùng một lúc. Trong trường hợp này, việc nhúng mã JavaScript không giúp ích được nhiều cho chúng ta. Tuy nhiên, có một số chiến lược có thể giúp trang của chúng ta hiển thị nhanh hơn.
Trước tiên, hãy nhớ rằng tất cả tập lệnh nội tuyến đều chặn trình phân tích cú pháp, nhưng đối với tập lệnh bên ngoài, chúng ta có thể thêm thuộc tính async
để bỏ chặn trình phân tích cú pháp. Huỷ nội tuyến rồi thử lại:
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Async</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body onload="measureCRP()">
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script async src="timing.js"></script>
</body>
</html>
JavaScript chặn trình phân tích cú pháp (ngoại):
JavaScript không đồng bộ (bên ngoài):
Tốt hơn nhiều! Sự kiện domContentLoaded
kích hoạt ngay sau khi HTML được phân tích cú pháp; trình duyệt biết không chặn JavaScript và vì không có trình phân tích cú pháp nào khác chặn tập lệnh nên việc tạo CSSOM cũng có thể diễn ra song song.
Ngoài ra, chúng ta có thể đưa cả CSS và JavaScript vào cùng dòng:
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Inlined</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
p {
font-weight: bold;
}
span {
color: red;
}
p span {
display: none;
}
img {
float: right;
}
</style>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script>
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
</script>
</body>
</html>
Lưu ý rằng thời gian domContentLoaded
thực sự giống với thời gian trong ví dụ trước; thay vì đánh dấu JavaScript là không đồng bộ, chúng ta đã đưa cả CSS và JS vào chính trang đó. Điều này làm cho trang HTML của chúng ta lớn hơn nhiều, nhưng điểm tích cực là trình duyệt không phải đợi để tìm nạp bất kỳ tài nguyên bên ngoài nào; mọi thứ đều có trong trang.
Như bạn có thể thấy, ngay cả với một trang rất cơ bản, việc tối ưu hoá đường dẫn kết xuất quan trọng cũng là một bài tập không hề đơn giản: chúng ta cần hiểu biểu đồ phần phụ thuộc giữa các tài nguyên khác nhau, chúng ta cần xác định những tài nguyên nào là "quan trọng" và chúng ta phải chọn trong số nhiều chiến lược về cách đưa các tài nguyên đó vào trang. Không có giải pháp chung cho vấn đề này; mỗi trang đều khác nhau. Bạn cần tự thực hiện một quy trình tương tự để tìm ra chiến lược tối ưu.
Tuy nhiên, hãy xem liệu chúng ta có thể lùi lại một chút để xác định một số mẫu hiệu suất chung hay không.
Mẫu hiệu suất
Trang đơn giản nhất có thể chỉ bao gồm mã đánh dấu HTML; không có CSS, không có JavaScript hoặc các loại tài nguyên khác. Để hiển thị trang này, trình duyệt phải bắt đầu yêu cầu, đợi tài liệu HTML đến, phân tích cú pháp tài liệu đó, tạo DOM và cuối cùng hiển thị tài liệu đó trên màn hình:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Critical Path: No Style</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
Thời gian giữa T0 và T1 ghi lại thời gian xử lý mạng và máy chủ. Trong trường hợp tốt nhất (nếu tệp HTML có dung lượng nhỏ), chỉ cần một lượt truy xuất mạng là có thể tìm nạp toàn bộ tài liệu. Do cách thức hoạt động của giao thức truyền tải TCP, các tệp lớn hơn có thể yêu cầu nhiều lượt đi và về hơn. Do đó, trong trường hợp tốt nhất, trang ở trên có một lộ trình kết xuất quan trọng (tối thiểu) là một vòng.
Bây giờ, hãy xem xét cùng một trang, nhưng có tệp CSS bên ngoài:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
Một lần nữa, chúng ta phải thực hiện một vòng lặp mạng để tìm nạp tài liệu HTML, sau đó mã đánh dấu được truy xuất cho chúng ta biết rằng chúng ta cũng cần tệp CSS; điều này có nghĩa là trình duyệt phải quay lại máy chủ và lấy CSS trước khi có thể hiển thị trang trên màn hình. Do đó, trang này phải thực hiện tối thiểu hai lượt đi và về trước khi có thể hiển thị. Xin nhắc lại, tệp CSS có thể thực hiện nhiều lượt đi và về, do đó, hãy nhấn mạnh vào "tối thiểu".
Dưới đây là một số thuật ngữ chúng tôi sử dụng để mô tả đường dẫn kết xuất quan trọng:
- Tài nguyên quan trọng: Tài nguyên có thể chặn quá trình kết xuất ban đầu của trang.
- Chiều dài đường dẫn quan trọng: Số lượt đi và về hoặc tổng thời gian cần thiết để tìm nạp tất cả tài nguyên quan trọng.
- Bytes quan trọng: Tổng số byte cần thiết để hiển thị lần đầu trang, là tổng kích thước tệp chuyển của tất cả tài nguyên quan trọng. Ví dụ đầu tiên của chúng ta, với một trang HTML duy nhất, chứa một tài nguyên quan trọng duy nhất (tài liệu HTML); chiều dài đường dẫn quan trọng cũng bằng một lượt truy cập mạng (giả sử tệp có kích thước nhỏ) và tổng số byte quan trọng chỉ là kích thước truyền của chính tài liệu HTML.
Bây giờ, hãy so sánh với các đặc điểm của đường dẫn quan trọng trong ví dụ trước về HTML và CSS:
- 2 tài nguyên quan trọng
- 2 lượt đi và về trở lên cho chiều dài đường dẫn quan trọng tối thiểu
- 9 KB byte quan trọng
Chúng ta cần cả HTML và CSS để tạo cây kết xuất. Do đó, cả HTML và CSS đều là tài nguyên quan trọng: CSS chỉ được tìm nạp sau khi trình duyệt nhận được tài liệu HTML, do đó, chiều dài đường dẫn quan trọng tối thiểu là hai lượt đi và về. Cả hai tài nguyên này có tổng cộng 9 KB byte quan trọng.
Bây giờ, hãy thêm một tệp JavaScript khác vào.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js"></script>
</body>
</html>
Chúng tôi đã thêm app.js
, đây vừa là một thành phần JavaScript bên ngoài trên trang vừa là một tài nguyên chặn trình phân tích cú pháp (tức là tài nguyên quan trọng). Tệ hơn, để thực thi tệp JavaScript, chúng ta phải chặn và đợi CSSOM; hãy nhớ rằng JavaScript có thể truy vấn CSSOM và do đó, trình duyệt sẽ tạm dừng cho đến khi tải style.css
xuống và tạo CSSOM.
Tuy nhiên, trên thực tế, nếu chúng ta xem "hệ thống thác nước mạng" của trang này, bạn sẽ thấy cả yêu cầu CSS và JavaScript đều được bắt đầu gần như cùng một lúc; trình duyệt nhận HTML, khám phá cả hai tài nguyên và bắt đầu cả hai yêu cầu. Do đó, trang hiển thị trong hình ảnh trước có các đặc điểm sau về đường dẫn quan trọng:
- 3 tài nguyên quan trọng
- 2 lượt đi và về trở lên cho chiều dài đường dẫn quan trọng tối thiểu
- 11 KB byte quan trọng
Bây giờ, chúng ta có 3 tài nguyên quan trọng với tổng số 11 KB byte quan trọng, nhưng chiều dài đường dẫn quan trọng vẫn là 2 lượt đi và về vì chúng ta có thể chuyển CSS và JavaScript song song. Việc tìm hiểu các đặc điểm của đường dẫn kết xuất quan trọng có nghĩa là bạn có thể xác định các tài nguyên quan trọng và cũng hiểu cách trình duyệt lên lịch tìm nạp các tài nguyên đó.
Sau khi trò chuyện với các nhà phát triển trang web, chúng tôi nhận thấy rằng JavaScript mà chúng tôi đưa vào trang không cần phải chặn; chúng tôi có một số số liệu phân tích và mã khác trong đó không cần chặn việc hiển thị trang. Với kiến thức đó, chúng ta có thể thêm thuộc tính async
vào phần tử <script>
để bỏ chặn trình phân tích cú pháp:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js" async></script>
</body>
</html>
Tập lệnh không đồng bộ có một số ưu điểm:
- Tập lệnh không còn chặn trình phân tích cú pháp và không nằm trong đường dẫn kết xuất quan trọng.
- Vì không có tập lệnh quan trọng nào khác, nên CSS không cần chặn sự kiện
domContentLoaded
. - Sự kiện
domContentLoaded
càng kích hoạt sớm thì logic ứng dụng khác càng có thể bắt đầu thực thi sớm.
Do đó, trang được tối ưu hoá của chúng ta hiện đã trở lại với 2 tài nguyên quan trọng (HTML và CSS), với chiều dài đường dẫn quan trọng tối thiểu là 2 lượt đi và về và tổng cộng 9 KB byte quan trọng.
Cuối cùng, nếu chỉ cần trang tính kiểu CSS để in, thì trang tính đó sẽ trông như thế nào?
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" media="print" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js" async></script>
</body>
</html>
Vì tài nguyên style.css chỉ dùng để in nên trình duyệt không cần chặn tài nguyên này để hiển thị trang. Do đó, ngay khi quá trình tạo DOM hoàn tất, trình duyệt sẽ có đủ thông tin để hiển thị trang. Do đó, trang này chỉ có một tài nguyên quan trọng (tài liệu HTML) và chiều dài đường dẫn kết xuất quan trọng tối thiểu là một lượt đi và về.