Việc xác định và giải quyết điểm tắc nghẽn hiệu suất đường dẫn quan trọng đòi hỏi phải có kiến thức tốt về các lỗi phổ biến. Hãy cùng thực hiện một chuyến đi thực tế và trích xuất các mẫu hiệu suất phổ biến để giúp bạn tối ưu hoá các trang của mình.
Việc tối ưu hoá đường dẫn hiển thị quan trọng cho phép trình duyệt vẽ trang nhanh nhất có thể: các trang nhanh hơn chuyển thành mức độ tương tác cao hơn, nhiều trang được xem hơn và cải thiện chuyển đổi. Để giảm thiểu lượng thời gian mà khách truy cập dành ra để xem một màn hình trống, chúng ta cần tối ưu hoá việc tải tài nguyên nào cũng như theo thứ tự nào.
Để giúp minh hoạ quá trình này, hãy bắt đầu với trường hợp đơn giản nhất có thể và xây dựng dần trang của chúng ta để thêm các tài nguyên, kiểu và logic ứng dụng bổ sung. Trong quá trình này, chúng tôi sẽ tối ưu hoá từng trường hợp; chúng tôi cũng sẽ xem vấn đề có thể xảy ra ở đâu.
Cho đến nay, chúng tôi chỉ tập trung vào những gì xảy ra trong trình duyệt sau khi tài nguyên (tệp CSS, JS hoặc HTML) có sẵn để 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 tôi sẽ giả định như sau:
- Một lượt trọn vòng qua mạng (độ trễ truyền tải) đến máy chủ có giá 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>
Chúng tôi sẽ bắt đầu bằng mã đánh dấu HTML cơ bản và một hình ảnh duy nhất, không phải CSS hoặc JavaScript. Hãy mở ra tiến trình của 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. Lưu ý rằng phần trong suốt của đường màu xanh dương biểu thị 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 liền mạch hiển thị 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 rất nhỏ (<4K), vì vậy, chúng ta chỉ cần một lượt khứ hồi để tìm nạp toàn bộ tệp. Kết quả là, tài liệu HTML mất khoảng 200 mili giây để tìm nạp, với một nửa thời gian dành cho việc 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 chúng thành mã thông báo và xây dựng cây DOM. Lưu ý rằng Công cụ cho nhà phát triển thuận tiện báo cáo thời gian cho sự kiện DOMContentLoaded ở dưới cùng (216 mili giây), cũng tương ứng với đường dọc màu xanh dương. Khoảng cách giữa thời điểm kết thúc tải xuống HTML và đường dọc màu xanh dương (DOMContentLoaded) là thời gian trình duyệt cần để xây dựng 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 tôi không chặn sự kiện domContentLoaded
. Hoá ra, chúng ta có thể tạo cây hiển thị và thậm chí vẽ trang mà không cần đợi 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 nhanh nội dung hiển thị đầu tiên. Trong thực tế, khi nói về đường dẫn hiển thị 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 lượt hiển thị ban đầu của trang, mặc dù chúng ta cũng nên cố gắng làm cho hình ảnh được vẽ 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: Công cụ cho nhà phát triển 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 đang 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 kết hợp
Trang "Trải nghiệm Hello World" của chúng tôi có vẻ đơn giản nhưng sẽ có rất nhiều vấn đề phức tạp. Trong thực tế, chúng ta không chỉ cần HTML: rất có thể, chúng ta sẽ có một biểu định kiểu CSS và một hoặc nhiều tập lệnh để thêm một số tương tác vào trang. Hãy thêm cả hai vào danh sách kết hợp và xem điều gì 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:
Nếu dùng 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 tôi, tất cả các yêu cầu đó trình duyệt gửi cùng một lúc. Tuy nhiên, xin lưu ý rằng hiện tại, sự khác biệt về thời gian giữa các 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ý của mình, 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 hiển thị.
- 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
sẽ 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 tôi phải chặn tệp CSS cho đến khi tệp đó tải xuống rồi mới có thể thực thi JavaScript.
Nếu chúng ta thay tập lệnh bên ngoài bằng một tập lệnh cùng dòng thì sao? Ngay cả khi tập lệnh được đặt 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 tạo CSSOM. Tóm lại, JavaScript cùng dòng cũng là tính năng chặn trình phân tích cú pháp.
Điều đó có nghĩa là mặc dù chặn trên CSS, nhưng việc cùng dòng tập lệnh có làm cho trang hiển thị nhanh hơn không? Hãy thử xem điều gì sẽ xảy ra.
JavaScript bên ngoài:
JavaScript cùng dòng:
Chúng ta giảm bớt một yêu cầu, nhưng cả onload
và domContentLoaded
lần đều giống nhau. Tại sao? Chúng tôi biết rằng việc JavaScript nằm cùng dòng hay bên ngoài không quan trọng, bởi vì ngay khi trình duyệt đạt đến thẻ tập lệnh, nó sẽ chặn và đợi cho đến khi CSSOM được tạo. Hơn nữa, trong ví dụ đầu tiên của chúng tôi, trình duyệt tải xuống song song cả CSS và JavaScript và chúng hoàn tất việc tải xuống cùng một lúc. Trong trường hợp này, việc chèn cùng dòng mã JavaScript không giúp ích nhiều cho chúng tôi. 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ả các tập lệnh cùng dòng đều đang chặn trình phân tích cú pháp, nhưng đối với các tập lệnh bên ngoài, chúng ta có thể thêm từ khoá "async" (không đồng bộ) để bỏ chặn trình phân tích cú pháp. Hãy hoàn tác cùng dòng của chúng ta và thử điều đó:
<!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 phân tích cú pháp (bên ngoài):
JavaScript không đồng bộ (bên ngoài):
Tốt hơn nhiều! Sự kiện domContentLoaded
sẽ kích hoạt ngay sau khi HTML được phân tích cú pháp; trình duyệt sẽ biết là không chặn JavaScript và vì không có tập lệnh phân tích cú pháp nào khác chặn nên quá trình tạo CSSOM cũng có thể tiến hành song song.
Ngoài ra, chúng tôi có thể đã chèn cả CSS và JavaScript:
<!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 như trong ví dụ trước; thay vì đánh dấu JavaScript là không đồng bộ, chúng tôi đã chuyển cả CSS và JS vào trang. Điều này làm cho trang HTML của chúng tôi lớn hơn nhiều, nhưng ưu điểm 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 ở ngay trên trang.
Như bạn có thể thấy, ngay cả với một trang rất đơn giản, việc tối ưu hoá đường dẫn hiển thị quan trọng là một bài tập không hề nhỏ: 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, xác định tài nguyên nào "quan trọng" và chúng ta phải chọn trong số các chiến lược khác nhau về cách đưa những tài nguyên đó lên trang. Không có giải pháp nào cho vấn đề này; mỗi trang đều khác nhau. Bạn cần tự mình làm theo 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 và xác định một số mẫu hiệu suất chung hay không.
Các mẫu hiệu suất
Trang đơn giản nhất có thể chỉ có mã đánh dấu HTML; không có CSS, không có JavaScript hay các loại tài nguyên khác. Để kết xuất trang này, trình duyệt phải khởi tạo yêu cầu, đợi tài liệu HTML đến, phân tích cú pháp, tạo DOM và cuối cùng là hiển thị 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 từ T0 đến T1 ghi lại thời gian xử lý của mạng và máy chủ. Trong trường hợp tốt nhất (nếu tệp HTML nhỏ), chỉ cần một lượt khứ hồi mạng tìm nạp toàn bộ tài liệu. Do cách hoạt động của giao thức truyền tải TCP, các tệp lớn hơn có thể cần nhiều lượt trọn vòng hơn. Do đó, trong trường hợp tốt nhất, trang trên chỉ có một đường dẫn hiển thị quan trọng trọn vòng (tối thiểu).
Bây giờ, hãy xem xét cùng một trang nhưng với 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 tôi phải trải qua một lượt khứ hồi mạng để tìm nạp tài liệu HTML và sau đó mã đánh dấu được truy xuất cho chúng tôi biết rằng chúng tôi 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à tải CSS trước khi có thể hiển thị trang trên màn hình. Do đó, trang này phải chịu tối thiểu hai lượt khứ hồi trước khi có thể hiển thị. Một lần nữa, tệp CSS có thể thực hiện nhiều lần khứ hồi, do đó nhấn mạnh vào giá trị "tối thiểu".
Hãy xác định từ vựng chúng ta sử dụng để mô tả đường dẫn hiển thị quan trọng:
- Tài nguyên quan trọng: Tài nguyên có thể chặn lượt hiển thị ban đầu của trang.
- Độ dài đường dẫn quan trọng: Số lượt trọn vòng 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.
- Số byte quan trọng: Tổng số byte cần thiết để hiển thị trang lần đầu tiên, đây 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 tôi, 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); độ dài đường dẫn quan trọng cũng bằng với một lượt trọn vòng mạng (giả sử tệp là 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 điều đó với các đặc điểm đường dẫn quan trọng của ví dụ HTML + CSS ở trên:
- 2 tài nguyên quan trọng
- 2 chuyến khứ hồi trở lên cho độ 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 hiển thị. 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 đó độ dài đường dẫn quan trọng tối thiểu là hai khứ hồi. Cả hai tài nguyên đều có tổng dung lượng là 9KB byte quan trọng.
Bây giờ, hãy thêm một tệp JavaScript bổ sung vào tập hợ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"></script>
</body>
</html>
Chúng tôi thêm app.js
. Đây vừa là một tài sả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 (quan trọng). Tệ hơn, để thực thi tệp JavaScript, chúng ta phải chặn và chờ 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 style.css
được tải xuống và CSSOM được tạo.
Tuy nhiên, trong thực tế, nếu xem xét "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 khởi tạo cùng một lúc; trình duyệt nhận được HTML, phát hiện cả hai tài nguyên và bắt đầu cả hai yêu cầu. Do đó, trang trên có các đặc điểm đường dẫn quan trọng sau:
- 3 tài nguyên quan trọng
- 2 chuyến khứ hồi trở lên cho độ dài đường dẫn quan trọng tối thiểu
- 11 KB byte quan trọng
Chúng tôi hiện có ba tài nguyên quan trọng tổng cộng lên đến 11KB các byte quan trọng, nhưng độ dài đường dẫn quan trọng của chúng tôi vẫn là hai lượt trọn vòng vì chúng tôi có thể chuyển song song CSS và JavaScript. Khi hiểu được các đặc điểm của đường dẫn hiển thị quan trọng, bạn sẽ có thể xác định những tài nguyên quan trọng và hiểu được cách trình duyệt sẽ lên lịch tìm nạp chúng. Hãy tiếp tục với ví dụ của chúng tôi.
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ố analytics và mã khác tại đó không cần chặn việc hiển thị trang của chúng tôi. Với thông tin đó, chúng ta có thể thêm thuộc tính "async" vào thẻ tập lệnh để 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 sau:
- Tập lệnh không còn chặn trình phân tích cú pháp và không còn là một phần của đường dẫn hiển thị 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 kích hoạt càng sớm thì logic ứng dụng khác có thể bắt đầu thực thi càng sớm.
Do đó, trang được tối ưu hoá của chúng tôi hiện quay trở lại hai tài nguyên quan trọng (HTML và CSS), với độ dài đường dẫn quan trọng tối thiểu là hai khứ hồi và tổng cộng 9KB byte quan trọng.
Cuối cùng, nếu biểu định kiểu CSS chỉ cần dùng để in, biểu định kiểu đó 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ỉ được dùng để in nên trình duyệt không cần chặn tài nguyên đó để hiển thị trang. Do đó, ngay khi quá trình xây dựng 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 duy nhất (tài liệu HTML) và độ dài đường dẫn hiển thị quan trọng tối thiểu là một lượt trọn vòng.