Cari tahu apa itu pemindai pramuat browser, bagaimana cara kerjanya untuk membantu performa, dan cara menghindarinya.
Salah satu aspek yang diabaikan dalam mengoptimalkan kecepatan halaman adalah mengetahui sedikit tentang internal browser. Browser melakukan pengoptimalan tertentu untuk meningkatkan performa dengan cara yang tidak dapat kita lakukan sebagai developer—tetapi hanya selama pengoptimalan tersebut tidak digagalkan secara tidak sengaja.
Salah satu pengoptimalan browser internal yang perlu dipahami adalah pemindai pramuat browser. Postingan ini akan membahas cara kerja pemindai pramuat—dan yang lebih penting, cara menghindari gangguan pada pemindai tersebut.
Apa yang dimaksud dengan pemindai pramuat?
Setiap browser memiliki parser HTML utama yang membuat token markup mentah dan memprosesnya menjadi model objek. Semua ini akan terus berjalan hingga parser dijeda saat menemukan resource pemblokir, seperti stylesheet yang dimuat dengan elemen <link>
, atau skrip yang dimuat dengan elemen <script>
tanpa atribut async
atau defer
.
Dalam kasus file CSS, rendering diblokir untuk mencegah flash of unstyled content (FOUC), yaitu saat versi halaman tanpa gaya dapat dilihat sebentar sebelum gaya diterapkan.
Browser juga memblokir penguraian dan rendering halaman saat menemukan elemen <script>
tanpa atribut defer
atau async
.
Alasannya adalah browser tidak dapat mengetahui dengan pasti apakah skrip tertentu akan mengubah DOM saat parser HTML utama masih melakukan tugasnya. Itulah sebabnya memuat JavaScript di akhir dokumen adalah praktik umum sehingga efek penguraian dan rendering yang diblokir menjadi marginal.
Ini adalah alasan yang baik mengapa browser harus memblokir penguraian dan rendering. Namun, pemblokiran salah satu langkah penting ini tidak diinginkan, karena dapat menghambat acara dengan menunda penemuan resource penting lainnya. Untungnya, browser melakukan yang terbaik untuk mengurangi masalah ini melalui parser HTML sekunder yang disebut pemindai pramuat.
Peran pemindai pramuat bersifat spekulatif, yang berarti pemindai ini memeriksa markup mentah untuk menemukan resource yang akan diambil secara oportunistik sebelum parser HTML utama menemukannya.
Cara mengetahui kapan pemindai pramuat berfungsi
Pemindai pramuat ada karena rendering dan penguraian diblokir. Jika kedua masalah performa ini tidak pernah ada, pemindai pramuat tidak akan terlalu berguna. Kunci untuk mengetahui apakah halaman web mendapatkan manfaat dari pemindai pramuat bergantung pada fenomena pemblokiran ini. Untuk melakukannya, Anda dapat memperkenalkan penundaan buatan untuk permintaan guna mengetahui tempat pemindai pramuat berfungsi.
Ambil halaman ini yang berisi teks dan gambar dasar dengan stylesheet sebagai contoh. Karena file CSS memblokir rendering dan penguraian, Anda akan memperkenalkan penundaan buatan selama dua detik untuk stylesheet melalui layanan proxy. Penundaan ini memudahkan untuk melihat di waterfall jaringan tempat pemindai pramuat berfungsi.
Seperti yang dapat Anda lihat di waterfall, pemindai pramuat menemukan elemen <img>
meskipun rendering dan penguraian dokumen diblokir. Tanpa pengoptimalan ini, browser tidak dapat mengambil sesuatu secara oportunistik selama periode pemblokiran, dan lebih banyak permintaan resource akan berurutan, bukan serentak.
Setelah contoh mainan tersebut, mari kita lihat beberapa pola di dunia nyata tempat pemindai pramuat dapat dikalahkan—dan apa yang dapat dilakukan untuk memperbaikinya.
Skrip async
yang dimasukkan
Misalkan Anda memiliki HTML di <head>
yang menyertakan beberapa JavaScript inline seperti ini:
<script>
const scriptEl = document.createElement('script');
scriptEl.src = '/yall.min.js';
document.head.appendChild(scriptEl);
</script>
Skrip yang dimasukkan adalah async
secara default, sehingga saat skrip ini dimasukkan, skrip tersebut akan berperilaku seolah-olah atribut async
diterapkan padanya. Artinya, kode akan berjalan sesegera mungkin dan tidak memblokir rendering. Kedengarannya optimal, bukan? Namun, jika Anda menganggap bahwa <script>
inline ini muncul setelah elemen <link>
yang memuat file CSS eksternal, Anda akan mendapatkan hasil yang kurang optimal:
Mari kita urai apa yang terjadi di sini:
- Pada 0 detik, dokumen utama akan diminta.
- Pada 1,4 detik, byte pertama permintaan navigasi akan tiba.
- Pada 2,0 detik, CSS dan gambar diminta.
- Karena parser diblokir untuk memuat stylesheet dan JavaScript inline yang memasukkan skrip
async
muncul setelah stylesheet tersebut pada 2,6 detik, fungsi yang disediakan skrip tidak tersedia sesegera mungkin.
Hal ini tidak optimal karena permintaan skrip hanya terjadi setelah stylesheet selesai didownload. Tindakan ini menunda skrip agar tidak berjalan sesegera mungkin. Sebaliknya, karena elemen <img>
dapat ditemukan dalam markup yang disediakan server, elemen tersebut akan ditemukan oleh pemindai pramuat.
Jadi, apa yang terjadi jika Anda menggunakan tag <script>
reguler dengan atribut async
, bukan memasukkan skrip ke DOM?
<script src="/yall.min.js" async></script>
Berikut hasilnya:
Mungkin ada beberapa godaan untuk menyarankan bahwa masalah ini dapat diatasi dengan menggunakan rel=preload
. Cara ini pasti akan berhasil, tetapi mungkin menimbulkan beberapa efek samping. Lagi pula, mengapa menggunakan rel=preload
untuk memperbaiki masalah yang dapat dihindari dengan tidak memasukkan elemen <script>
ke DOM?
Pemuatan awal "memperbaiki" masalah di sini, tetapi menimbulkan masalah baru: skrip async
dalam dua demo pertama—meskipun dimuat di <head>
—dimuat dengan prioritas "Rendah", sedangkan stylesheet dimuat dengan prioritas "Tertinggi". Pada demo terakhir saat skrip async
dimuat sebelumnya, stylesheet masih dimuat dengan prioritas "Tertinggi", tetapi prioritas skrip telah dipromosikan ke "Tinggi".
Saat prioritas resource dinaikkan, browser akan mengalokasikan lebih banyak bandwidth ke resource tersebut. Artinya, meskipun stylesheet memiliki prioritas tertinggi, prioritas skrip yang dinaikkan dapat menyebabkan pertentangan bandwidth. Hal ini dapat menjadi faktor pada koneksi yang lambat, atau jika resource cukup besar.
Jawabannya di sini cukup sederhana: jika skrip diperlukan selama startup, jangan mengalahkan pemindai pramuat dengan memasukkannya ke DOM. Lakukan eksperimen sesuai kebutuhan dengan penempatan elemen <script>
, serta dengan atribut seperti defer
dan async
.
Pemuatan lambat dengan JavaScript
Pemuatan lambat adalah metode yang bagus untuk menghemat data, yang sering diterapkan pada gambar. Namun, terkadang pemuatan lambat salah diterapkan pada gambar yang "di atas lipatan".
Hal ini menimbulkan potensi masalah terkait visibilitas resource yang terkait dengan pemindai pramuat, dan dapat menunda waktu yang diperlukan untuk menemukan referensi ke gambar, mendownloadnya, mendekodenya, dan menampilkannya. Mari kita ambil contoh markup gambar ini:
<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
Penggunaan awalan data-
adalah pola umum dalam loader lambat yang didukung JavaScript. Saat gambar di-scroll ke area pandang, loader lambat akan menghapus awalan data-
, yang berarti bahwa dalam contoh sebelumnya, data-src
menjadi src
. Pembaruan ini akan meminta browser mengambil resource.
Pola ini tidak bermasalah hingga diterapkan ke gambar yang ada di area pandang selama startup. Karena pemindai pramuat tidak membaca atribut data-src
dengan cara yang sama seperti atribut src
(atau srcset
), referensi gambar tidak ditemukan sebelumnya. Lebih buruk lagi, gambar tertunda pemuatnya hingga setelah JavaScript loader lambat mendownload, mengompilasi, dan mengeksekusi.
Bergantung pada ukuran gambar—yang mungkin bergantung pada ukuran area pandang—gambar tersebut dapat menjadi elemen kandidat untuk Largest Contentful Paint (LCP). Jika pemindai pramuat tidak dapat mengambil resource gambar secara spekulatif sebelumnya—mungkin selama titik saat stylesheet halaman memblokir rendering—LCP akan terpengaruh.
Solusinya adalah mengubah markup gambar:
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
Ini adalah pola optimal untuk gambar yang ada di area pandang selama startup, karena pemindai pramuat akan menemukan dan mengambil resource gambar dengan lebih cepat.
Hasil dalam contoh sederhana ini adalah peningkatan LCP sebesar 100 milidetik pada koneksi lambat. Hal ini mungkin tidak tampak seperti peningkatan yang besar, tetapi itu adalah peningkatan yang besar jika Anda mempertimbangkan bahwa solusinya adalah perbaikan markup cepat, dan sebagian besar halaman web lebih kompleks daripada kumpulan contoh ini. Artinya, kandidat LCP mungkin harus bersaing untuk mendapatkan bandwidth dengan banyak resource lainnya, sehingga pengoptimalan seperti ini menjadi semakin penting.
Gambar latar CSS
Ingat bahwa pemindai pramuat browser memindai markup. Alat ini tidak memindai jenis resource lain, seperti CSS yang mungkin melibatkan pengambilan gambar yang dirujuk oleh properti background-image
.
Seperti HTML, browser memproses CSS menjadi model objeknya sendiri, yang dikenal sebagai CSSOM. Jika resource eksternal ditemukan saat CSSOM dibuat, resource tersebut akan diminta pada saat penemuan, dan bukan oleh pemindai pramuat.
Misalnya, kandidat LCP halaman Anda adalah elemen dengan properti background-image
CSS. Berikut adalah hal yang terjadi saat resource dimuat:
Dalam hal ini, pemindai pramuat tidak terlalu dikalahkan karena tidak terlibat. Meskipun demikian, jika kandidat LCP di halaman berasal dari properti CSS background-image
, Anda harus melakukan pramuat gambar tersebut:
<!-- Make sure this is in the <head> below any
stylesheets, so as not to block them from loading -->
<link rel="preload" as="image" href="lcp-image.jpg">
Petunjuk rel=preload
tersebut kecil, tetapi membantu browser menemukan gambar lebih cepat dari biasanya:
Dengan petunjuk rel=preload
, kandidat LCP akan ditemukan lebih cepat, sehingga menurunkan waktu LCP. Meskipun petunjuk tersebut membantu memperbaiki masalah ini, opsi yang lebih baik adalah menilai apakah kandidat LCP gambar Anda harus dimuat dari CSS atau tidak. Dengan tag <img>
, Anda akan memiliki kontrol yang lebih besar atas pemuatan gambar yang sesuai untuk area pandang sekaligus memungkinkan pemindai pramuat menemukannya.
Menyisipkan terlalu banyak resource
Penyisipan adalah praktik yang menempatkan resource di dalam HTML. Anda dapat menyisipkan stylesheet dalam elemen <style>
, skrip dalam elemen <script>
, dan hampir semua resource lainnya menggunakan encoding base64.
Menyertakan resource dapat lebih cepat daripada mendownloadnya karena permintaan terpisah tidak dikeluarkan untuk resource tersebut. Video tersebut ada di dokumen, dan dimuat secara instan. Namun, ada kekurangan yang signifikan:
- Jika Anda tidak meng-cache HTML—dan Anda tidak dapat melakukannya jika respons HTML bersifat dinamis—resource yang disisipkan tidak akan pernah di-cache. Hal ini memengaruhi performa karena resource yang disisipkan tidak dapat digunakan kembali.
- Meskipun Anda dapat meng-cache HTML, resource yang disisipkan tidak dibagikan antar-dokumen. Hal ini mengurangi efisiensi penyimpanan dalam cache dibandingkan dengan file eksternal yang dapat di-cache dan digunakan kembali di seluruh origin.
- Jika Anda melakukan inline terlalu banyak, Anda akan menunda pemindai pramuat untuk menemukan resource nanti dalam dokumen, karena mendownload konten tambahan yang di-inline tersebut memerlukan waktu lebih lama.
Ambil halaman ini sebagai contoh. Dalam kondisi tertentu, kandidat LCP adalah gambar di bagian atas halaman, dan CSS berada dalam file terpisah yang dimuat oleh elemen <link>
. Halaman ini juga menggunakan empat font web yang diminta sebagai file terpisah dari resource CSS.
Sekarang, apa yang terjadi jika CSS dan semua font disisipkan sebagai resource base64?
Dampak penyematan menghasilkan konsekuensi negatif untuk LCP dalam contoh ini—dan untuk performa secara umum. Versi halaman yang tidak menyisipkan apa pun akan merender gambar LCP dalam waktu sekitar 3,5 detik. Halaman yang menyisipkan semuanya tidak akan merender gambar LCP hingga lebih dari 7 detik.
Ada lebih banyak hal yang berperan di sini daripada sekadar pemindai pramuat. Menyisipkan font bukanlah strategi yang bagus karena base64 adalah format yang tidak efisien untuk resource biner. Faktor lain yang berperan adalah resource font eksternal tidak didownload kecuali jika ditentukan diperlukan oleh CSSOM. Saat font tersebut disisipkan sebagai base64, font tersebut akan didownload, baik diperlukan untuk halaman saat ini maupun tidak.
Bisakah pramuat meningkatkan kualitas di sini? Oke. Anda dapat memuat gambar LCP secara offline dan mengurangi waktu LCP, tetapi membengkakkan HTML yang berpotensi tidak dapat di-cache dengan resource yang disematkan akan memiliki konsekuensi performa negatif lainnya. First Contentful Paint (FCP) juga terpengaruh oleh pola ini. Dalam versi halaman yang tidak memiliki inline, FCP-nya kira-kira 2,7 detik. Dalam versi yang semuanya disisipkan, FCP kira-kira 5,8 detik.
Berhati-hatilah saat menyisipkan sesuatu ke dalam HTML, terutama resource yang dienkode base64. Secara umum, hal ini tidak direkomendasikan, kecuali untuk resource yang sangat kecil. Seminimal mungkin gunakan inline, karena terlalu banyak inline akan berisiko.
Merender markup dengan JavaScript sisi klien
Tidak ada keraguan: JavaScript pasti memengaruhi kecepatan halaman. Developer tidak hanya mengandalkannya untuk memberikan interaktivitas, tetapi juga cenderung mengandalkannya untuk mengirimkan konten itu sendiri. Hal ini menghasilkan pengalaman developer yang lebih baik dalam beberapa hal; tetapi manfaat bagi developer tidak selalu berarti manfaat bagi pengguna.
Salah satu pola yang dapat mengalahkan pemindai pramuat adalah merender markup dengan JavaScript sisi klien:
Jika payload markup terdapat dalam dan dirender sepenuhnya oleh JavaScript di browser, semua resource dalam markup tersebut secara efektif tidak terlihat oleh pemindai pramuat. Hal ini menunda penemuan resource penting, yang tentu saja memengaruhi LCP. Dalam contoh ini, permintaan untuk gambar LCP tertunda secara signifikan jika dibandingkan dengan pengalaman yang dirender server yang setara dan tidak memerlukan JavaScript untuk muncul.
Hal ini sedikit menyimpang dari fokus artikel ini, tetapi efek rendering markup pada klien jauh lebih dari sekadar mengalahkan pemindai pramuat. Misalnya, memperkenalkan JavaScript untuk mendukung pengalaman yang tidak memerlukannya akan menyebabkan waktu pemrosesan yang tidak perlu yang dapat memengaruhi Interaction to Next Paint (INP). Merender markup dalam jumlah yang sangat besar di klien lebih cenderung menghasilkan tugas yang lama dibandingkan dengan jumlah markup yang sama yang dikirim oleh server. Alasannya—selain pemrosesan tambahan yang melibatkan JavaScript—adalah browser melakukan streaming markup dari server dan mengelompokkan rendering sedemikian rupa sehingga cenderung membatasi tugas yang lama. Di sisi lain, markup yang dirender klien ditangani sebagai satu tugas monolitik, yang dapat memengaruhi INP halaman.
Solusi untuk skenario ini bergantung pada jawaban atas pertanyaan ini: Apakah ada alasan mengapa markup halaman Anda tidak dapat disediakan oleh server, bukan dirender di klien? Jika jawabannya "tidak", rendering sisi server (SSR) atau markup yang dibuat secara statis harus dipertimbangkan jika memungkinkan, karena akan membantu pemindai pramuat menemukan dan mengambil resource penting secara oportunistik sebelumnya.
Jika halaman Anda memerlukan JavaScript untuk melampirkan fungsi ke beberapa bagian markup halaman, Anda tetap dapat melakukannya dengan SSR, baik dengan JavaScript vanilla, maupun hidrasi untuk mendapatkan yang terbaik dari keduanya.
Membantu pemindai pramuat membantu Anda
Pemindai pramuat adalah pengoptimalan browser yang sangat efektif yang membantu halaman dimuat lebih cepat selama startup. Dengan menghindari pola yang mengalahkan kemampuannya untuk menemukan resource penting terlebih dahulu, Anda tidak hanya membuat pengembangan menjadi lebih sederhana, tetapi juga menciptakan pengalaman pengguna yang lebih baik yang akan memberikan hasil yang lebih baik dalam banyak metrik, termasuk beberapa data web.
Sebagai ringkasan, berikut hal-hal yang perlu Anda perhatikan dari postingan ini:
- Pemindai pramuat browser adalah parser HTML sekunder yang memindai sebelum parser utama jika diblokir untuk secara oportunistik menemukan resource yang dapat diambil lebih cepat.
- Resource yang tidak ada dalam markup yang disediakan oleh server pada permintaan navigasi awal tidak dapat ditemukan oleh pemindai pramuat. Cara pemindai pramuat dapat dikalahkan dapat mencakup (tetapi tidak terbatas pada):
- Memasukkan resource ke dalam DOM dengan JavaScript, baik itu skrip, gambar, stylesheet, atau apa pun yang akan lebih baik dalam payload markup awal dari server.
- Pemuatan lambat gambar atau iframe paruh atas menggunakan solusi JavaScript.
- Merender markup di klien yang mungkin berisi referensi ke sub-resource dokumen menggunakan JavaScript.
- Pemindai pramuat hanya memindai HTML. Alat ini tidak memeriksa konten resource lain—terutama CSS—yang mungkin menyertakan referensi ke aset penting, termasuk kandidat LCP.
Jika, karena alasan apa pun, Anda tidak dapat menghindari pola yang memengaruhi kemampuan pemindai pramuat secara negatif untuk mempercepat performa pemuatan, pertimbangkan petunjuk resource rel=preload
. Jika Anda menggunakan rel=preload
, uji di alat lab untuk memastikan bahwa alat tersebut memberikan efek yang diinginkan. Terakhir, jangan memuat ulang terlalu banyak resource, karena jika Anda memprioritaskan semuanya, tidak ada yang akan menjadi prioritas.
Resource
- "Skrip asinkron" yang berisi skrip dianggap berbahaya
- Cara Pramuat Browser Mempercepat Pemuatan Halaman
- Memuat aset penting terlebih dahulu untuk meningkatkan kecepatan pemuatan
- Membuat koneksi jaringan lebih awal untuk meningkatkan kecepatan halaman yang dirasakan
- Mengoptimalkan Largest Contentful Paint (LCP)
Banner besar dari Unsplash, oleh Mohammad Rahmani .