Untuk mengidentifikasi dan mengatasi bottleneck performa jalur rendering penting, diperlukan pengetahuan yang baik tentang kendala umum. Mari kita ikuti tur langsung dan mengetahui pola performa umum yang akan membantu mengoptimalkan halaman Anda.
Dengan mengoptimalkan jalur rendering penting, browser dapat mempercantik halaman secepat mungkin: halaman yang lebih cepat menghasilkan interaksi yang lebih tinggi, lebih banyak halaman yang dilihat, dan peningkatan konversi. Untuk meminimalkan waktu yang dihabiskan pengunjung untuk melihat layar kosong, kita perlu mengoptimalkan sumber daya mana yang dimuat dan bagaimana urutannya.
Untuk membantu menggambarkan proses ini, mari kita mulai dengan kasus paling sederhana dan secara bertahap membangun laman kita untuk menyertakan sumber daya, gaya, dan logika aplikasi tambahan. Dalam prosesnya, kita akan mengoptimalkan setiap kasus; kita juga akan melihat di mana terjadi kesalahan.
Sejauh ini kita telah berfokus secara eksklusif pada apa yang terjadi di browser setelah sumber daya (file CSS, JS, atau HTML) tersedia untuk diproses. Kita telah mengabaikan waktu yang diperlukan untuk mengambil resource, baik dari cache maupun dari jaringan. Kita akan asumsikan hal berikut:
- Perjalanan bolak-balik jaringan (latensi propagasi) ke server memerlukan waktu 100 md.
- Waktu respons server adalah 100 md untuk dokumen HTML dan 10 md untuk semua file lainnya.
Pengalaman 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>
Kita akan mulai dengan markup HTML dasar dan gambar tunggal; tanpa CSS atau JavaScript. Mari kita buka linimasa Jaringan di Chrome DevTools dan periksa waterfall resource yang dihasilkan:
Seperti yang diharapkan, file HTML memerlukan waktu sekitar 200 md untuk didownload. Perhatikan bahwa bagian transparan pada garis biru menyatakan waktu tunggu browser di jaringan tanpa menerima byte respons sementara bagian yang padat menampilkan waktu untuk menyelesaikan pengunduhan setelah byte respons pertama diterima. Ukuran download HTML kecil (<4K), sehingga kita hanya perlu satu perjalanan bolak-balik untuk mengambil file lengkap. Hasilnya, dokumen HTML memerlukan waktu sekitar 200 md untuk diambil, dengan separuh waktu dihabiskan untuk jaringan dan setengah lagi menunggu respons server.
Bila konten HTML tersedia, browser akan mengurai byte, mengonversinya menjadi token, dan membangun hierarki DOM. Perhatikan bahwa DevTools dengan mudah melaporkan waktu untuk peristiwa DOMContentLoaded di bagian bawah (216 md), yang juga sesuai dengan garis vertikal biru. Kesenjangan antara akhir download HTML dan garis vertikal biru (DOMContentLoaded) adalah waktu yang dibutuhkan browser untuk membangun hierarki DOM—dalam hal ini, hanya beberapa milidetik.
Perhatikan bahwa "foto keren" kita tidak memblokir peristiwa domContentLoaded
. Ternyata, kita dapat membuat hierarki render dan bahkan menggambar halaman tanpa menunggu setiap aset di halaman: tidak semua resource penting untuk menghasilkan cat pertama yang cepat. Sebenarnya, bila kita membicarakan jalur rendering penting, biasanya kita membicarakan markup HTML, CSS, dan JavaScript. Gambar tidak menghalangi render awal halaman, meskipun kita juga harus mencoba untuk melukis gambar sesegera mungkin.
Dengan demikian, peristiwa load
(juga dikenal sebagai onload
), diblokir pada gambar: DevTools melaporkan peristiwa onload
pada 335 md. Ingat kembali bahwa peristiwa onload
menandai titik saat semua resource yang diperlukan halaman telah didownload dan diproses; pada titik ini, indikator lingkaran berputar pemuatan dapat berhenti berputar di browser (garis vertikal merah dalam waterfall).
Menambahkan JavaScript dan CSS ke dalam campuran
"Pengalaman Hello World" kami halaman tampak sederhana tetapi banyak yang terjadi di balik layar. Pada praktiknya, kita juga akan memerlukan lebih dari sekadar HTML: kemungkinannya adalah, kita akan memiliki stylesheet CSS dan satu atau beberapa skrip untuk menambahkan beberapa interaktivitas ke halaman. Mari kita tambahkan keduanya ke campuran dan lihat apa yang terjadi:
<!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>
Sebelum menambahkan JavaScript dan CSS:
Dengan JavaScript dan CSS:
Menambahkan file CSS dan JavaScript eksternal akan menambahkan dua permintaan ekstra ke waterfall, yang semuanya dikirim oleh browser pada waktu yang hampir bersamaan. Namun, perhatikan bahwa sekarang ada perbedaan waktu yang jauh lebih kecil antara peristiwa domContentLoaded
dan onload
.
Apa yang terjadi?
- Tidak seperti contoh HTML biasa, kita juga perlu mengambil dan mem-parse file CSS untuk membangun CSSOM, dan kita membutuhkan DOM maupun CSSOM untuk membangun pohon render.
- Karena halaman juga berisi file JavaScript pemblokir parser, peristiwa
domContentLoaded
diblokir sampai file CSS didownload dan diurai: karena JavaScript mungkin mengkueri GCLID, kita harus memblokir file CSS sampai terdownload sebelum kita dapat mengeksekusi JavaScript.
Bagaimana jika kita mengganti skrip eksternal dengan skrip inline? Bahkan jika skrip disisipkan secara langsung ke dalam laman, browser tidak dapat mengeksekusinya hingga GCLID dibangun. Singkatnya, JavaScript yang disisipkan juga merupakan pemblokiran parser.
Dengan demikian, meskipun memblokir CSS, apakah inline skrip akan membuat halaman dirender lebih cepat? Mari kita coba dan lihat apa yang terjadi.
JavaScript Eksternal:
JavaScript Inline:
Kita mengurangi satu permintaan, tetapi waktu onload
dan domContentLoaded
pada dasarnya sama. Mengapa? Seperti yang kita ketahui, tidak masalah jika JavaScript inline atau eksternal, karena segera setelah browser mencapai tag skrip, browser akan memblokir dan menunggu hingga GCLID dibangun. Lebih jauh, dalam contoh pertama, browser mengunduh CSS maupun JavaScript secara bersamaan dan selesai mengunduhnya pada waktu yang hampir bersamaan. Dalam contoh ini, menyisipkan kode JavaScript tidak akan banyak membantu. Namun, ada beberapa strategi yang dapat membuat halaman kita dirender lebih cepat.
Pertama-tama, ingat bahwa semua skrip inline merupakan pemblokir parser, tetapi untuk skrip eksternal kita dapat menambahkan atribut "async" kata kunci untuk membuka blokir parser. Mari kita urungkan penyisipan dan mencobanya:
<!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 yang memblokir parser (eksternal):
JavaScript Async (eksternal):
Jauh lebih baik! Peristiwa domContentLoaded
diaktifkan tepat setelah HTML diuraikan; browser tahu untuk tidak memblokir JavaScript dan karena tidak ada skrip pemblokir parser lainnya, konstruksi dimulai juga dapat berlangsung secara paralel.
Atau, kita bisa membuat CSS dan JavaScript menjadi inline:
<!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>
Perhatikan bahwa waktu domContentLoaded
pada dasarnya sama seperti pada contoh sebelumnya; alih-alih menandai JavaScript sebagai asinkron, kita telah menyisipkan baik CSS maupun JS ke dalam halaman itu sendiri. Hal ini membuat halaman HTML kita menjadi jauh lebih besar, tetapi sisi terbaliknya adalah browser tidak harus menunggu mengambil sumber daya eksternal mana pun - semuanya ada di laman itu.
Seperti yang dapat Anda lihat, walaupun dengan halaman yang sangat sederhana, mengoptimalkan jalur rendering penting adalah latihan yang tidak biasa: kita perlu memahami grafik dependensi di antara berbagai sumber daya, kita perlu mengidentifikasi sumber daya mana yang "penting", dan kita harus memilih di antara berbagai strategi tentang cara menyertakan sumber daya tersebut pada laman. Tidak ada satu solusi untuk masalah ini; setiap halaman berbeda. Anda perlu mengikuti proses serupa untuk mengetahui strategi yang optimal.
Dengan demikian, mari kita lihat apakah kita bisa mundur sejenak dan mengidentifikasi beberapa pola performa umum.
Pola performa
Halaman paling sederhana yang mungkin terdiri dari markup HTML saja; tanpa CSS, tanpa JavaScript, atau jenis sumber daya lainnya. Untuk merender halaman ini, browser harus memulai permintaan, menunggu dokumen HTML tiba, mengurainya, membuat DOM, lalu akhirnya merendernya di layar:
<!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>
Waktu antara T0 dan T1 merekam waktu pemrosesan jaringan dan server. Dalam kasus terbaik (jika file HTML-nya kecil), hanya perlu satu kali bolak-balik di jaringan untuk mengambil dokumen lengkap. Karena cara kerja protokol transpor TCP, file yang lebih besar mungkin memerlukan lebih banyak perjalanan bolak-balik. Akibatnya, dalam kasus terbaik, halaman di atas memiliki satu jalur rendering penting (minimum) bolak-balik.
Sekarang, mari kita pertimbangkan halaman yang sama tetapi dengan file CSS eksternal:
<!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>
Sekali lagi, kita dikenai perjalanan bolak-balik jaringan untuk mengambil dokumen HTML, lalu markup yang diambil akan memberi tahu bahwa kita juga memerlukan file CSS; ini berarti bahwa browser harus kembali ke server dan mendapatkan CSS sebelum dapat merender halaman di layar. Oleh karena itu, halaman ini harus bolak-balik setidaknya dua kali agar dapat ditampilkan. Sekali lagi, file CSS mungkin perlu beberapa kali bolak-balik, sehingga penekanannya ditetapkan pada "minimum".
Mari kita definisikan kosa kata yang kita gunakan untuk menjelaskan jalur rendering penting:
- Resource Penting: Resource yang dapat memblokir rendering awal halaman.
- Panjang Jalur Penting: Jumlah perjalanan bolak-balik, atau total waktu yang diperlukan untuk mengambil semua resource penting.
- Byte Penting: Jumlah total byte yang diperlukan untuk sampai ke render pertama halaman, yang merupakan jumlah ukuran file transfer dari semua resource penting. Contoh pertama kami, dengan satu laman HTML, berisi satu sumber daya penting (dokumen HTML); panjang jalur kritis juga sama dengan satu perjalanan bolak-balik jaringan (dengan asumsi filenya kecil), dan total byte penting hanyalah ukuran transfer dari dokumen HTML itu sendiri.
Sekarang mari kita bandingkan dengan karakteristik jalur kritis dari contoh HTML + CSS di atas:
- 2 resource penting
- 2 perjalanan bolak-balik atau lebih untuk panjang jalur penting minimum
- 9 KB byte penting
Kita memerlukan HTML dan CSS untuk membangun pohon render. Akibatnya, HTML maupun CSS sama-sama merupakan sumber daya penting: CSS hanya diambil setelah browser mendapatkan dokumen HTML, sehingga panjang jalur pentingnya adalah minimal dua perjalanan bolak-balik. Total jumlah kedua resource menjadi 9 KB byte penting.
Sekarang, mari tambahkan file JavaScript tambahan ke dalam campuran.
<!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>
Kita telah menambahkan app.js
, yang merupakan aset JavaScript eksternal pada halaman dan sumber daya pemblokiran parser (yang penting). Lebih buruk lagi, untuk mengeksekusi file JavaScript kita harus memblokir dan menunggu dimulai; ingat bahwa JavaScript dapat melakukan kueri terhadap GCLID dan karenanya browser akan dijeda hingga style.css
didownload dan DevTools dibangun.
Dengan demikian, pada praktiknya jika kita melihat "Waterfall jaringan" halaman ini, Anda akan melihat bahwa permintaan CSS dan JavaScript dimulai pada waktu yang hampir bersamaan; browser mendapatkan HTML, menemukan kedua sumber daya, dan memulai kedua permintaan. Hasilnya, halaman di atas memiliki karakteristik jalur kritis berikut:
- 3 resource penting
- 2 perjalanan bolak-balik atau lebih untuk panjang jalur penting minimum
- 11 KB byte penting
Sekarang kita memiliki tiga sumber daya penting yang berjumlah hingga 11 KB byte penting, tetapi panjang jalur penting kita masih dua perjalanan bolak-balik karena kita dapat mentransfer CSS dan JavaScript secara paralel. Dengan mengetahui karakteristik jalur rendering penting, Anda dapat mengidentifikasi resource penting dan memahami cara browser menjadwalkan pengambilannya. Mari kita lanjutkan dengan contoh kita.
Setelah mengobrol dengan developer situs kami, kami menyadari bahwa JavaScript yang kami sertakan di halaman kami tidak perlu diblokir; terdapat beberapa analisis dan kode lain di dalamnya yang tidak perlu memblokir rendering halaman. Dengan pengetahuan itu, kita dapat menambahkan fungsi ke tag skrip untuk berhenti memblokir parser:
<!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>
Skrip asinkron memiliki beberapa keunggulan:
- Skrip tidak lagi merupakan pemblokiran parser dan bukan bagian dari jalur rendering penting.
- Karena tidak ada skrip penting lainnya, CSS tidak perlu memblokir peristiwa
domContentLoaded
. - Semakin cepat peristiwa
domContentLoaded
dipicu, semakin cepat logika aplikasi lainnya dapat mulai dieksekusi.
Hasilnya, halaman yang dioptimalkan kini kembali ke dua sumber daya penting (HTML dan CSS), dengan panjang jalur kritis minimum dua perjalanan bolak-balik, dan total 9 KB byte penting.
Terakhir, jika stylesheet CSS hanya perlu dicetak, seperti apa tampilannya?
<!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>
Karena resource style.css hanya digunakan untuk publikasi cetak, browser tidak perlu memblokirnya untuk merender halaman. Oleh karena itu, begitu konstruksi DOM selesai, browser memiliki cukup informasi untuk merender halaman. Hasilnya, halaman ini hanya memiliki satu sumber daya penting (dokumen HTML), dan panjang jalur rendering penting minimum adalah satu perjalanan bolak-balik.