Bab 23

Caching, PWA & Offline

Sejak bab 11, data halaman datang lewat jaringan. Bab 21 menghemat beban muat awalnya. Bab ini menyambung keduanya dengan pertanyaan yang tinggal: mengapa berkas dan data yang sama harus diambil berulang-ulang, dan apa yang terjadi saat jaringan hilang. Perkakas jawabannya tiga lapis, cache bawaan browser, Cache API, dan service worker, lalu dirangkai menjadi PWA.

Kuka berdiri di dapur mengambil kaleng dari lemari persediaan yang pintunya terbuka berisi bahan bertatakan tanggal, sementara lewat jendela dapur terlihat sebuah pasar dengan atap terasah.
Ambil dari lemari dulu, ke pasar hanya kalau stoknya kosong.

Mengapa caching, PWA, dan offline ada

Cache adalah salinan hasil permintaan yang disimpan, supaya permintaan yang sama tidak perlu dikerjakan ulang. Di front end, kerja yang paling mahal adalah menyeberangi jaringan. Bab 21 sudah membuktikannya lewat ukuran bundle dan gambar, dan bab 11 menunjukkan satu permintaan fetch bisa memakan waktu lebih lama daripada seluruh kode halaman. Kalau data itu jarang berubah, mengambilnya ulang setiap kunjungan adalah kerja yang dibuang begitu saja.

Alasan keduanya lebih kasar: jaringan bisa hilang. Naik kereta, masuk lift, atau listrik di suatu tempat padam, dan halaman yang mengandalkan server mendadak jadi kertas kosong. Halaman yang tetap menyala saat koneksi putus disebut jalan offline, kondisi tanpa jaringan. Kemampuan itu bukan fitur tambahan yang muncul sendiri, ia dibangun dari salinan yang sudah disimpan sebelumnya.

Rangkaian perkakas untuk dua masalah itu akhirnya diberi satu nama. PWA atau Progressive Web App adalah halaman web yang memenuhi syarat tertentu agar bisa dipasang seperti aplikasi dan tetap jalan tanpa jaringan. Bab ini membedah syaratnya satu per satu, karena setiap syarat adalah konsep penting yang berguna bahkan tanpa PWA.

HTTP cache: lemari bawaan browser

Lapisan pertama sudah bekerja tanpa kamu tulis satu baris pun. Browser menyimpan salinan berkas yang pernah diambil, gaya, gambar, JavaScript, dan memakai ulang salinannya di kunjungan berikutnya. Simpanan otomatis ini disebut HTTP cache.

Cara kerjanya ada di jawaban server. Setiap jawaban HTTP membawa header, baris-baris keterangan yang menyertai isi jawaban. Salah satunya bernama Cache-Control, perintah tertulis dari server tentang bagaimana salinannya boleh disimpan. Nilai max-age=31536000 artinya salinan ini sah dipakai 31536000 detik, sekitar satu tahun. Selama masa itu, permintaan berikutnya untuk berkas yang sama dijawab dari simpanan browser, tidak ada paket yang menyeberang jaringan.

Batas lapisan ini jelas. Aturannya ditulis server dan browser hanya patuh, halaman tidak bisa ikut memutuskan. Ia juga tidak menolong saat halaman sendiri dibuka pertama kali tanpa jaringan, karena lemari masih kosong. Untuk ikut memutuskan, halaman butuh lemari yang kuncinya ada di tangannya sendiri.

Cache API: lemari yang bisa kamu buka

Lapisan kedua bernama Cache API, antarmuka JavaScript untuk membuat dan membaca simpanan permintaan dan jawaban milik halaman sendiri. Perkakasnya kecil. caches.open("nama") membuka lemari dengan nama itu, atau membuatnya kalau belum ada. cache.add(url) mengambil sebuah alamat lewat fetch lalu langsung menyimpan jawabannya. cache.match(url) membaca kembali salinannya. Semua lewat Promise dan await, persis kebiasaan bab 11.

Simpanannya rapi per origin. Origin adalah asal-usul halaman, gabungan skema, domain, dan port, misalnya https://www.argakuka.com. Halaman dari satu origin tidak bisa membaca lemari origin lain, jadi salinanmu tidak tertukar dengan situs tetangga.

Service worker: penjaga di pintu

Kuka membaca selembar daftar aturan stok sambil berdiri di dapur, di depannya seorang asisten berapron berdiri di pintu belakang yang pintunya terbuka menuju pasar, satu tangan memegang kertas aturan, tangan lain menghalau membawa tas belanja.
Asisten yang mengatur lalu lintas bahan, dari lemari atau dari pasar.

Lapisan ketiga mengambil keputusan. Service worker adalah skrip yang dipasang browser sebagai penjaga di pintu jaringan sebuah halaman. Setiap permintaan halaman itu, berkas, gambar, data API, lewat tangannya lebih dulu. Ia boleh meneruskan ke jaringan, boleh menjawab dari cache, boleh mencampur keduanya.

Karena penjaga bisa menjawab dari cache, halaman tetap hidup saat jaringan mati, dan itulah offline yang sebenarnya. Service worker juga hidup terpisah dari tab. Tab ditutup, penjaga tetap terpasang dan bekerja untuk kunjungan berikutnya. Pemasangannya satu baris, navigator.serviceWorker.register("/sw.js"), tetapi perhatikan isinya, service worker wajib berupa berkas tersendiri, ia tidak bisa ditanam di dalam HTML halaman.

Strategi: cache dulu atau jaringan dulu

Memasang penjaga saja belum cukup, kamu harus menuliskan aturannya. Dua strategi paling umum punya nama. Cache-first berarti tanya lemari dulu, ada salinannya langsung disajikan, kosong baru belanja ke jaringan. Cepat, dan cocok untuk berkas yang jarang berubah seperti gaya dan gambar logo. Network-first berarti ke jaringan dulu, jawaban terbaru disajikan dan disalin ke lemari, dan salinan hanya jadi penyelamat saat jaringan gagal. Sedikit lebih lambat, tetapi datanya segar, dan cocok untuk data yang sering berubah.

Memilihnya bukan soal benar dan salah, melainkan soal mana yang lebih penting untuk data itu, kecepatan atau kesegaran. Satu halaman boleh memakai keduanya sekaligus, cache-first untuk berkas, network-first untuk data.

PWA: dari halaman jadi aplikasi

Setelah caching dan offline beres, PWA tinggal melengkapinya dengan dua syarat kecil. Pertama, halaman disajikan lewat HTTPS, sambungan terenkripsi yang menjadi rumah semua service worker. Kedua, ada manifest, berkas kecil berformat JSON yang berisi nama aplikasi, ikon, dan warna temanya. Browser membaca manifest itu lalu menawarkan pengunjung untuk memasang halaman di layar utama, lengkap dengan ikonnya sendiri dan tanpa bilah alamat. Sekali terpasang, aplikasi membuka halaman lewat penjaga yang sudah menunggu, dan jalan walau pasarnya tutup.

Batas dan jebakan

Semua perkakas bab ini berjalan hanya di konteks aman, halaman lewat HTTPS atau localhost di komputer sendiri. Dibuka lewat file langsung dari folder, Cache API tidak tersedia. Cache juga tidak punya aturan membereskan diri sendiri, salinan versi lama tetap duduk di lemari sampai dihapus, dan itulah sebabnya nama lemari biasanya diberi versi, catatan-v1 berubah jadi catatan-v2 saat isi berubah, yang lama dihapus. Terakhir, service worker yang salah aturan bisa menyimpan halaman yang rusak untuk waktu lama, karena setiap kunjungan dijawab penjaga yang sama. Aturannya ditulis dengan sengaja, dan ujilah sebelum dipasang.

Analogi: dapur dan lemari persediaan

Satu gambar untuk semuanya. Kamu juru masak, jaringan adalah pasar, cache adalah lemari persediaan di dapur, dan HTTP cache adalah lemari yang isinya diatur perjanjian dengan penjual, berganti stok hanya sesuai tanggal yang disepakati di kertasnya. Cache API adalah lemari kedua yang kamu beli sendiri dan kamu isi sendiri dengan tas belanja dari pasar. Service worker adalah asisten dapur yang berdiri di pintu belakang, setiap bahan lewat tangannya, dan ia memegang aturan stok yang kamu tulis, ambil dari lemari dulu, atau belanja dulu dan simpan sisanya. Saat pasar tutup, asisten tetap bisa menyajikan dari lemari, itulah offline. Dan papan nama warung di depan rumah beserta menunya, itulah manifest yang membuat dapur resmi disebut warung, PWA.

Pasangan contoh

Contohnya memuat daftar catatan dari /api/catatan dengan strategi cache-first. Vanilanya membuka lemari bernama catatan-v1, menanyakan salinan lewat cache.match, dan langsung menampilkannya kalau ada. Kalau kosong, fetch ke jaringan berjalan, jawabannya disalin lewat cache.put sebelum ditampilkan. Perhatikan jawaban.clone(), satu jawaban hanya boleh dibaca sekali, jadi satu salinan untuk lemari dan satu untuk layar.

Vanilla JavaScript

async function muatCatatan() {
  const cache = await caches.open("catatan-v1");
  const tersimpan = await cache.match("/api/catatan");

  if (tersimpan) {
    tampilkan(await tersimpan.json());
    return;
  }

  const jawaban = await fetch("/api/catatan");
  await cache.put("/api/catatan", jawaban.clone());
  tampilkan(await jawaban.json());
}

React

function useCacheFirst(url) {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function muat() {
      const cache = await caches.open("catatan-v1");
      const tersimpan = await cache.match(url);
      if (tersimpan) {
        setData(await tersimpan.json());
        return;
      }
      const jawaban = await fetch(url);
      await cache.put(url, jawaban.clone());
      setData(await jawaban.json());
    }
    muat();
  }, [url]);

  return data;
}

Strategi cache-first-nya sama persis di kedua versi, tanya lemari dulu, belanja kalau kosong, simpan sisanya. Yang berbeda hanya bungkusnya. Vanilanya fungsi biasa yang halaman panggil sendiri. React membungkus logika yang sama dalam hook, fungsi pembungkus state dan efek dari bab 17, sehingga komponen mana pun bisa memuat data dengan satu baris useCacheFirst("/api/catatan") tanpa menulis ulang aturan lemarnya. Cache tidak mengganti fetch, ia berdiri di depannya.

Demo Hidup

Demo di bawah memakai Cache API sungguhan, lapisan inti bab ini, untuk menyimpan salinan berkas /enhancements.css dari halaman yang sedang kamu baca. Tekan tombol simpan, berkas diambil lewat fetch lalu masuk lemari. Tekan tombol baca, isinya kembali dari cache tanpa menyentuh jaringan. Setelah tersimpan, tombol baca tetap jalan walaupun koneksi putus, coba lepas koneksi lalu tekan lagi.

Demo ini menyimpan dan membaca berkas dari Cache Storage memakai Cache API. Perhatikan bahwa baca dari cache tidak mengirim permintaan apa pun ke server.

Cache masih kosong.

Begini cara baca demonya. caches.open("demo-bab-23") membuka satu lemari milik origin halaman ini. cache.add() mengerjakan fetch dan cache.put sekaligus, ambil lalu simpan. cache.match() mengambil salinan langsung dari Cache Storage, karena itu ia jalan tanpa jaringan. Ukuran berkas dihitung dari isi salinannya sendiri, bukan dari server. Kalau demo bilang Cache API tidak tersedia, halaman sedang dibuka di luar konteks aman, dan itu batas yang dibahas di atas.

Ringkasan bab ini. Menyeberangi jaringan itu mahal dan tidak selalu bisa, karena itu hasilnya disimpan. HTTP cache menyimpan otomatis sesuai header Cache-Control dari server. Cache API memberi halaman lemari sendiri yang bisa dibuka lewat caches.open, cache.add, dan cache.match, per origin. Service worker adalah penjaga di pintu jaringan yang memutuskan dari mana permintaan dijawab, cache-first untuk kecepatan, network-first untuk kesegaran. Manifest dan HTTPS melengkapinya menjadi PWA yang bisa dipasang dan jalan offline. Bab berikutnya menutup seri dengan menyatukan semuanya dalam satu proyek.

Rujukan