Memuat CSS dan JS

Elemen link, penempatan script, dan perilaku defer versus async.

Daftar isi

CSS dan JS tidak pernah ditulis di dalam satu berkas dengan HTML, sehingga cara head dan body memuat keduanya menentukan kecepatan tampil halaman. Topik ini membahas link rel=“stylesheet” di dalam head, penempatan script dari kebiasaan lama sampai atribut modern, dan perbandingan perilaku defer dengan async. Setelah mengikuti topik ini, strategi pemuatan untuk setiap skrip dapat dipilih dengan alasan yang jelas.

Berkas CSS dimuat melalui link beratribut rel=“stylesheet” yang diletakkan di dalam head. Posisi itu bukan kebiasaan semata, karena browser menunda penayangan halaman hingga CSS utamanya selesai dimuat. Penundaan itu disengaja, yaitu mencegah halaman tampil tanpa gaya sesaat lalu berubah bentuk ketika CSS-nya tiba.

Sifat penghambat perenderan (render blocking) tersebut membuat berkas CSS yang besar memperlambat tampil pertama halaman, sehingga gaya yang benar-benar kritis dijaga tetap ringkas. Atribut media memberi celah untuk gaya yang tidak segera dibutuhkan, misalnya media="print" yang berlaku untuk pencetakan. Browser tetap mengunduh berkas tersebut, tetapi tidak menjadikannya penghambat karena kondisi medianya tidak cocok dengan perangkat yang sedang digunakan.

Skrip di akhir body sebagai kebiasaan lama#

Elemen script klasik, yaitu script tanpa atribut khusus, menghentikan parser begitu ditemui. Parser berhenti mengurai markup, mengunduh berkas skripnya, mengeksekusinya sampai selesai, dan baru melanjutkan sisa dokumen. Script yang diletakkan di dalam head dengan cara ini menahan seluruh konten di bawahnya, padahal skrip sering kali belum bisa berbuat apa-apa karena elemen yang hendak disentuhnya belum diurai.

Kebiasaan lama menjawab masalah itu dengan memindahkan seluruh script ke akhir body, tepat sebelum tag penutup body. Konten di atas skrip selesai diurai lebih dahulu, sehingga skrip menemukan elemen yang dicarinya dan halaman tampil sebelum skrip selesai dieksekusi. Kelemahannya, pengunduhan skrip baru dimulai setelah seluruh dokumen diurai, dan halaman belum dapat dianggap siap berinteraksi sebelum skrip terakhir selesai. Atribut defer dan async menyelesaikan kedua masalah itu dengan lebih rapi.

defer dan async pada script eksternal#

Kedua atribut ini berlaku untuk script eksternal, yaitu script yang memuat berkasnya melalui atribut src. Keduanya mengubah perilaku yang sama, yaitu pengunduhan skrip berjalan paralel dengan parsing dan tidak lagi menghentikannya, sementara waktu eksekusinya berbeda. Perbedaan ketiga penempatan itu dirangkum pada tabel berikut.

AtributPengunduhanWaktu eksekusiUrutan antarskrip
tanpa atributparser berhenti sampai selesaisegera setelah ditemuisesuai urutan dokumen
deferparalel dengan parsingsetelah parsing selesai, sebelum DOMContentLoadedsesuai urutan dokumen
asyncparalel dengan parsingbegitu berkasnya siaptidak dijamin

Atribut defer memasukkan skrip ke antrean yang urutannya dijamin, yaitu sesuai urutan kemunculan di dokumen. Seluruh skrip dalam antrean itu dijalankan setelah parsing selesai namun sebelum event DOMContentLoaded terpicu. Skrip beratribut defer dengan demikian aman menyentuh elemen apa pun di dokumen tanpa memedulikan posisi tag-nya di head. Atribut async menempuh jalan yang berbeda, yaitu skripnya dieksekusi begitu berkasnya selesai diunduh, sehingga urutan antarskrip yang saling bergantung tidak dapat diandalkan.

Skrip bertipe module, yaitu script dengan type="module", berperilaku seperti defer secara bawaan tanpa atribut yang ditulis. Perilaku itu berlaku pula untuk module inline, sehingga script module tidak pernah menghentikan parsing di tengah dokumen. Atribut async tetap dapat ditambahkan pada script module bila urutannya memang tidak penting.

Perilaku defer dan async dapat diamati langsung. Demo berikut memuat satu berkas index.js dua kali, sekali beratribut defer dan sekali async, lalu mencatat setiap eksekusi beserta status dokumen pada saat itu ke dalam daftar berurutan. Buka daftar itu, muat ulang demo beberapa kali, kemudian amati entri mana yang posisinya bergeser.

Kode sumber demo
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="utf-8">
  <title>Urutan eksekusi script</title>
  <style>
    body { font-family: sans-serif; max-width: 32rem; margin: 0 auto; padding: 0 1rem; color: #222222; }
    h1 { font-size: 1.2rem; }
    li { margin: 0.25rem 0; }
  </style>
  <script>
    // pencatat urutan: entri yang lahir sebelum daftar siap disimpan ke antrean lebih dahulu
    window.__antrean = [];
    window.tulis = function (pesan) {
      const daftar = document.getElementById("catatan");
      if (daftar) {
        const antrean = window.__antrean.splice(0);
        for (const p of antrean) {
          const butir = document.createElement("li");
          butir.textContent = p;
          daftar.appendChild(butir);
        }
        const butir = document.createElement("li");
        butir.textContent = pesan;
        daftar.appendChild(butir);
      } else {
        window.__antrean.push(pesan);
      }
    };
  </script>
  <script src="index.js" defer></script>
  <script src="index.js" async></script>
</head>
<body>
  <h1>Catatan urutan eksekusi</h1>
  <ol id="catatan"></ol>
  <script>
    window.tulis("skrip inline di body dijalankan, status dokumen: " + document.readyState);
    document.addEventListener("DOMContentLoaded", () => {
      window.tulis("DOMContentLoaded terpicu, status dokumen: " + document.readyState);
    });
  </script>

<script>
// index.js dijalankan dua kali oleh demo: sekali beratribut defer, sekali async
const skrip = document.currentScript;
let mode = "tanpa atribut defer/async";
if (skrip) {
  if (skrip.defer) {
    mode = "defer";
  } else if (skrip.async) {
    mode = "async";
  }
}
window.tulis("index.js beratribut " + mode + " dijalankan, status dokumen: " + document.readyState);

</script></body>
</html>

Empat jenis entri muncul pada catatan demo, yaitu dua eksekusi index.js, satu skrip inline di body, dan satu event DOMContentLoaded. Entri yang dijamin posisinya adalah skrip inline di body yang berjalan selagi status dokumen masih loading, eksekusi defer yang selalu berstatus interactive setelah parsing selesai, dan DOMContentLoaded yang selalu menutup catatan. Posisi entri async tidak dijamin sama sekali, dan pergeseran posisinya antarpemuatan ulang justru memperlihatkan sifat aslinya. Pada berkas lokal yang unduhannya nyaris seketika, entri async kerap jatuh di akhir catatan, sedangkan pada jaringan yang lambat entri yang sama dapat muncul jauh lebih awal.

Skrip berikut memperlihatkan pola aksi yang didaftarkan setelah dokumen siap. Skrip itu menggunakan API browser sehingga tidak dapat dieksekusi di lingkungan Node, tetapi polanya tetap terbaca.

js Mendaftarkan aksi setelah dokumen siap
// tidak-dijalankan
document.addEventListener("DOMContentLoaded", () => {
  const status = document.querySelector("#status");
  status.textContent = "Halaman siap dibaca.";
});

Pola ini merupakan pengganti kebiasaan akhir body untuk skrip yang tidak diberi defer, karena aksinya baru berjalan setelah seluruh markup siap. Dengan defer, pola seperti itu jarang diperlukan lagi karena eksekusi skripnya sendiri sudah menunggu parsing selesai.

Berkas utuh pemuatan terurut#

Berkas utuh berikut menggabungkan seluruh keputusan pemuatan yang dibahas, yaitu stylesheet di head, script eksternal beratribut defer di head, dan satu skrip inline pendek di akhir body.

html pemuatan.html
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Halaman Pemuatan Terurut</title>
  <link rel="stylesheet" href="gaya.css">
  <script src="logika.js" defer></script>
</head>
<body>
  <h1>Halaman Pemuatan Terurut</h1>
  <p id="status">Menunggu skrip berjalan.</p>
  <script>
    // skrip inline di akhir body berjalan setelah seluruh markup di atasnya siap
    document.getElementById("status").textContent = "Halaman siap dibaca.";
  </script>
</body>
</html>

Baris 7 memuat gaya.css dan membuat browser menahan penayangan hingga CSS-nya siap. Baris 8 memuat logika.js dengan defer, sehingga pengunduhannya paralel tetapi eksekusinya menunggu parsing selesai. Skrip inline pada baris 13 sampai 16 berjalan selagi parsing dan mengganti teks elemen status. Pola seperti ini memang paling aman ditulis di akhir body karena elemen di atasnya sudah pasti siap.

Kesalahan umum#

Script besar tanpa defer diletakkan di head. Parsing halaman berhenti sampai berkasnya selesai diunduh dan dieksekusi, sehingga konten tampil terlambat. Tambahkan atribut defer atau pindahkan skripnya ke akhir body.

Skrip menyentuh elemen sebelum elemennya ada. Error seperti Uncaught TypeError: Cannot read properties of null muncul ketika querySelector dijalankan sebelum markup target diurai. Eksekusi skripnya dengan defer, atau daftarkan aksinya pada event DOMContentLoaded.

Rantai skrip yang saling bergantung diberi async. Urutan eksekusinya tidak dijamin, sehingga satu skrip dapat berjalan sebelum dependensinya. Gunakan defer untuk seluruh rantai skrip yang bergantung tersebut.

Ringkasan#

  • link rel=“stylesheet” diletakkan di head dan bersifat penghambat perenderan, sehingga berkas CSS-nya dijaga tetap ringkas.
  • Script klasik menghentikan parsing, dan kebiasaan memindahkannya ke akhir body kini digantikan atribut defer.
  • defer mengeksekusi skrip eksternal setelah parsing dalam urutan dokumen, sedangkan async mengeksekusinya begitu berkasnya siap tanpa jaminan urutan.
  • script type=“module” berperilaku seperti defer secara bawaan, dan DOMContentLoaded terpicu setelah seluruh skrip defer selesai.

Referensi#