Prinsip pertama dari 54 Best Practices Umum: minimalkan JavaScript. Bab ini ngejelasin kenapa, lewat metrik yang JavaScript paling ngerusak: INP.

JavaScript, baik kode theme kamu maupun script app, jalan di main thread dan bersaing langsung sama kemampuan browser buat ngerender konten dan merespons input. Terlalu banyak JavaScript, dimuat di waktu yang salah, atau ditulis gak efisien, itu penyebab utama INP buruk dan LCP telat. Tujuannya: load cuma yang perlu, cuma waktu perlu: defer script non-kritis, import modul saat interaksi bukan saat page load, dan hindari framework berat kalau Liquid dan CSS bisa ngerjain hal yang sama. Dan waktu ada masalah, profil penyebab spesifiknya: kode first-party yang jelek sama merusaknya dengan script pihak ketiga yang buruk.

Memahami INP

Interaction to Next Paint ngukur responsivitas halaman terhadap interaksi pengguna sepanjang satu kunjungan. Data Chrome: 90% waktu pengguna terjadi setelah page load, dan INP nangkep itu dengan ngelaporin interaksi terburuk atau hampir terburuk di seluruh kunjungan, bukan cuma tap pertama (pendahulunya, FID, cuma ngukur interaksi pertama). Baik ≤ 200ms, buruk > 500ms, dinilai di persentil ke-75 kunjungan, jadi satu interaksi cepat di lab gak nunjukin apa-apa.

Kenapa penting: pembeli tap Add to Cart dan gak terjadi apa-apa selama setengah detik, atau pemilih varian terasa lambat, pengalamannya terasa rusak walaupun halaman dimuat cepat. Halaman bisa punya LCP sempurna tapi tetap terasa lambat kalau JavaScript yang bersaing di main thread nunda interaksi. Tiap app terinstal, script analitik, dan file JavaScript theme nyumbang ke beban main thread yang INP ukur.

1. Tiga Fase

Tiap interaksi dipecah jadi:

  1. Input delay: waktu antara aksi pengguna (tap, klik, keypress) dan browser mulai ngejalanin event handler. Terjadi waktu main thread udah sibuk ngejalanin JavaScript lain. Kalau script pihak ketiga lagi ngejalanin long task waktu pengguna tap, browser gak bisa mulai memproses tap sampai task itu selesai.
  2. Processing time: waktu ngejalanin event handler yang terpasang ke interaksi: JavaScript theme, app, dan framework yang nembak sebagai respons. Banyak handler bisa nembak buat satu interaksi (pointerdown, mousedown, click).
  3. Presentation delay: waktu browser ngitung ulang style, layout, composite layer, dan paint update visual. Perubahan DOM besar, recalculation CSS mahal, dan forced synchronous layout nambah fase ini.

Totalnya = latensi interaksi. Kunjungan berakhir waktu halaman di-unload, dan nilainya reset waktu halaman dipulihin dari back/forward cache.

2. Cara Nemuin

  1. Field data dulu (CrUX atau RUM dari 57 Performance Dasar, Metrik, dan Cara Ngukur): halaman mana yang INP-nya buruk, dan kalau ada RUM, interaksi spesifik mana. Yang umum lambat di toko Shopify: pemilih varian, buka/tutup cart drawer, mega menu, input pencarian, filter di koleksi.
  2. Kurangi total JavaScript sebelum debug mendalam, buat nurunin kontensi dasar main thread yang bikin input delay: audit semua app, hapus yang gak dipakai, cek apakah app yang “dihapus” masih nyuntik kode, timbang biaya vs manfaat app yang tersisa, cari fungsi duplikat antar app.
  3. Profil interaksi spesifik di Performance panel DevTools: Record → lakukan interaksi lambat → Stop → cari long task (sudut merah) di track Main → klik buat lihat fungsi yang jalan. Tab Summary nunjukin breakdown scripting/rendering/painting, yang ngasih tau fase mana yang dominan.

3. Masalah INP Khas Shopify

  1. Reflow pemilih varian: waktu varian dipilih, JavaScript ngupdate harga, ketersediaan, gambar, dan teks tombol sekaligus, tiap update DOM bisa memicu style recalculation dan layout. Batch pembacaan dan penulisan DOM biar gak ada forced synchronous layout, dan pakai requestAnimationFrame buat nunda update visual ke frame berikutnya.
  2. Animasi cart drawer dengan width/height/top memicu layout tiap frame. Pakai transform: translateX() dari 60 Performance CSS dan Font > Animasi: transform dan opacity.
  3. Hover mega menu yang ngerender DOM besar waktu hover bikin presentation delay panjang. Kalau kontennya udah di DOM tapi tersembunyi, toggle visibilitas jauh lebih cepat dari nyisipin elemen baru (tapi lihat trade-off DOM besar di bagian lazy DOM).
  4. Script pihak ketiga saat interaksi: analitik yang dengerin klik dan ngejalanin kode sinkron di handler nambah processing time ke tiap interaksi. Audit lewat tab Event Listeners di panel Elements.
  5. Jumlah stylesheet besar bikin style recalculation mahal saat interaksi, dari bab 60.

4. Membaca Profil Pemilih Varian

Buka halaman produk dengan DevTools, Performance, aktifkan Screenshots, Record, klik swatch (Black → Red), tunggu sedetik, Stop. Di rekaman: event klik di track Interactions nunjukin total latensi, dan di track Main di bawahnya: input delay = aktivitas scripting sebelum handler mulai, processing time = handler dan JavaScript yang dipicunya (cek call stack: kode theme, app, atau analitik), presentation delay = entri Recalculate Style, Layout, Paint, Composite Layers setelah JavaScript selesai.

Diagnosisnya: input delay terbesar → JavaScript lain ngeblokir main thread, kurangi pihak ketiga. Processing terbesar → handler-nya kebanyakan kerja, lihat call stack. Presentation terbesar → update DOM memicu layout mahal, cari forced synchronous layout (segitiga merah di Performance panel) atau banyak style recalculation.

5. TBT sebagai Proxy Lab

Total Blocking Time dari tes lab berkorelasi sama INP. TBT cuma ngukur blocking main thread selama page load, bukan interaksi setelahnya, tapi indikator yang berguna: 800ms TBT saat load + INP buruk di field → ngurangin kerja main thread yang ketemu saat load kemungkinan besar juga memperbaiki INP. Tapi Lighthouse gak bisa ngukur INP kunjungan nyata, field data wajib. Alat lain: Performance Monitor (CPU, jumlah DOM node, recalculation per detik secara live), ekstensi Web Vitals (INP real-time dengan elemen pemicunya), dan CPU throttling 4x buat simulasi perangkat lambat.

defer dan async

Waktu browser ketemu <script src> tanpa defer/async, dia berhenti parse HTML, download script, eksekusi, baru lanjut. Gak ada yang di bawah tag itu yang ditemukan atau dirender sampai script selesai. Dua biayanya: render tertunda (FCP dan LCP) dan discovery resource keblokir (preload scanner berhenti, gambar dan stylesheet di bawahnya gak di-fetch).

1. defer buat Script Theme

Script defer download di latar dan eksekusi berurutan sesuai urutan dokumen setelah HTML selesai di-parse tapi sebelum DOMContentLoaded. Ini pilihan tepat buat hampir semua script theme: yang bergantung DOM bisa ngandelin dokumen lengkap, yang saling bergantung jalan sesuai urutan, dan browser parse tanpa gangguan:

<head>
  <script src="{{ 'theme.js' | asset_url }}" defer></script>
  <script src="{{ 'product-form.js' | asset_url }}" defer></script>
  <script src="{{ 'cart-drawer.js' | asset_url }}" defer></script>
</head>

Ketiganya download paralel, eksekusi berurutan setelah parsing.

2. async buat Pihak Ketiga yang Independen

Script async download di latar dan eksekusi begitu selesai download, tanpa jaminan urutan, dan eksekusinya tetap bisa ngeblokir main thread sebentar. Buat script yang sepenuhnya independen, gak bergantung DOM atau script lain: analitik dan tracking pixel:

<script src="https://analytics.example.com/tracker.js" async></script>

Kalau script pihak ketiga bergantung DOM atau script lain, pakai defer.

3. Yang Gak Boleh Di-defer

  1. Anti-flicker snippet A/B testing yang nyembunyiin konten sampai varian dipilih. Di-defer bikin flash konten asli sebelum varian diterapin. Kalau gak ada tes yang aktif, hapus seluruhnya (63 Performance Apps dan Pihak Ketiga).
  2. Script yang ngubah konten viewport awal sebelum render: di-defer bikin flash atau layout shift.

4. Hapus yang Gak Perlu, Load Kondisional

Sebelum mutusin defer, tanya apakah script-nya perlu sama sekali. Tiap script, bahkan yang di-defer, ngonsumsi bandwidth dan main thread. Ngehapus selalu lebih baik dari defer. Dan gak semua halaman butuh semua script: pakai object template buat load cuma di halaman yang butuh:

<head>
  {%- comment -%} Core theme scripts load on every page {%- endcomment -%}
  <script src="{{ 'theme.js' | asset_url }}" defer></script>
  {%- comment -%} Load product form logic only on product pages {%- endcomment -%}
  {% if template.name == 'product' %}
    <script src="{{ 'product-form.js' | asset_url }}" defer></script>
  {% endif %}
  {%- comment -%} Load the cart drawer only when the cart type is drawer {%- endcomment -%}
  {% if settings.cart_type == 'drawer' %}
    <script src="{{ 'cart-drawer.js' | asset_url }}" defer></script>
  {% endif %}
  {%- comment -%} Load search predictions only on pages with a search form {%- endcomment -%}
  {% if template.name == 'search' or settings.predictive_search_enabled %}
    <script src="{{ 'search.js' | asset_url }}" defer></script>
  {% endif %}
</head>

❌ sinkron di <head>, ngeblokir parsing

<script src="{{ 'vendor.js' | asset_url }}"></script>
<script src="{{ 'theme.js' | asset_url }}"></script>

✅ defer, paralel dan berurutan

<script src="{{ 'vendor.js' | asset_url }}" defer></script>
<script src="{{ 'theme.js' | asset_url }}" defer></script>

Cek di Network panel (script sinkron = bar solid yang ngeblokir resource berikutnya) dan Performance > Insights (“Render blocking requests”).

Load JavaScript Saat Interaksi

Ngeload JavaScript buat fitur yang gak dipakai di muka nambah ukuran bundle, nunda render awal, ngeblokir main thread waktu parsing dan eksekusi, dan ngerusak INP dan TBT. Load kode cuma waktu pengguna nunjukin niat pakai fiturnya: bundle awal kecil, page load cepat, INP membaik, dan fiturnya tetap terasa instan karena kodenya load paralel sama aksi pengguna.

Caranya import() dinamis di dalam event listener. Pemicunya: klik (paling umum), hover/focus (sinyal niat, preload sebelum klik biar terasa instan), visibilitas/scroll (masuk viewport), idle (requestIdleCallback/setTimeout).

Klik, dengan { once: true } biar cuma sekali:

<button id="chat-widget-btn">Chat with us</button>
<script>
  const chatBtn = document.getElementById("chat-widget-btn");
  chatBtn.addEventListener(
    "click",
    (e) => {
      e.preventDefault();
      import("{{ 'chat-widget.js' | asset_url }}")
        .then((module) => module.default)
        .then((ChatWidget) => ChatWidget.init())
        .catch((err) => console.error("Failed to load chat:", err));
    },
    { once: true }
  );
</script>

Hover/focus, dengan satu promise bersama karena once itu per listener:

// `once` is per listener, so share one promise to init only once.
let loaded;
function loadFeature() {
  loaded ??= import("./feature.js").then((m) => m.init());
}
button.addEventListener("mouseenter", loadFeature, { once: true });
button.addEventListener("focus", loadFeature, { once: true });

Visibilitas lewat IntersectionObserver:

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      import("./feature.js").then((m) => m.init());
      observer.disconnect();
    }
  });
});
observer.observe(document.querySelector("#lazy-component"));

Idle:

if ("requestIdleCallback" in window) {
  requestIdleCallback(() => {
    import("./analytics.js").then((m) => m.init());
  });
} else {
  setTimeout(() => {
    import("./analytics.js").then((m) => m.init());
  }, 2000);
}

Kasus nyata: chat widget (load waktu tombol chat diklik), video player (tampilin thumbnail + tombol play, load player waktu diklik), dan konten Liquid mahal di dialog (render cangkang, fetch section-nya waktu dibuka, pola dari 58 Performance Liquid > Defer Konten Dialog):

<div class="video-container" data-video-id="abc123">
  <img src="{{ 'video-thumbnail.jpg' | asset_url }}" alt="Video thumbnail">
  <button class="play-btn">Play Video</button>
</div>
<script>
  document.querySelector('.play-btn').addEventListener('click', async function() {
    const container = this.closest('.video-container');
    const videoId = container.dataset.videoId;
    const { VideoPlayer } = await import("{{ 'video-player.js' | asset_url }}");
    container.innerHTML = '';
    const player = new VideoPlayer(container, videoId);
    player.play();
  }, { once: true });
</script>

Debounce, Throttle, dan Yield

Event handler eksekusi sinkron di main thread. Handler mahal ngeblokir browser dari merespons input, langsung ngerusak INP (fase processing time). Masalah umum: komputasi berat di tiap scroll/resize, pengukuran DOM sinkron di handler, banyak operasi yang memicu layout, operasi mahal di tiap keystroke.

1. Debounce

Nunda eksekusi sampai pengguna berhenti. Buat input pencarian, resize, validasi form:

function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), wait);
  };
}
// Search input: wait for the user to stop typing
const searchInput = document.querySelector("#search");
const debouncedSearch = debounce((event) => {
  performSearch(event.target.value);
}, 300);
searchInput.addEventListener("input", debouncedSearch);

Ini yang dipakai predictive search di 38 Search dan Predictive Search.

2. Throttle

Membatasi frekuensi eksekusi. Buat scroll, mouse move, resize:

function throttle(func, limit) {
  let inThrottle;
  return function (...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}
// Scroll tracking: update at most every 100 ms
const throttledScroll = throttle(() => {
  updateScrollPosition();
}, 100);
window.addEventListener("scroll", throttledScroll);

3. Passive Listener

Ngasih tau browser handler-nya gak bakal manggil preventDefault(), jadi browser bisa ngoptimasi scroll dan touch tanpa nunggu handler:

// Good: passive listener, the browser can optimize
document.addEventListener("touchstart", handleTouch, { passive: true });
document.addEventListener("wheel", handleWheel, { passive: true });
// Bad: non-passive, the browser must wait for the handler
document.addEventListener("touchstart", handleTouch);

4. requestAnimationFrame

Batch update DOM dan sinkron sama siklus paint browser:

// Bad: updates on every scroll event
window.addEventListener("scroll", () => {
  element.style.transform = `translateY(${window.scrollY}px)`;
});
// Good: batched with requestAnimationFrame
let ticking = false;
window.addEventListener(
  "scroll",
  () => {
    if (!ticking) {
      requestAnimationFrame(() => {
        element.style.transform = `translateY(${window.scrollY}px)`;
        ticking = false;
      });
      ticking = true;
    }
  },
  { passive: true }
);

5. Pecah Long Task

Bagi handler panjang jadi potongan dengan setTimeout atau scheduler.yield(), biar browser bisa merespons input di antaranya:

// Bad: long synchronous task blocks the main thread
button.addEventListener("click", () => {
  for (let i = 0; i < 10000; i++) {
    processItem(i);
  }
});
// Good: yield to browser between chunks
button.addEventListener("click", async () => {
  const chunkSize = 50;
  for (let i = 0; i < 10000; i += chunkSize) {
    for (let j = i; j < Math.min(i + chunkSize, 10000); j++) {
      processItem(j);
    }
    // Yield to the browser
    await new Promise((resolve) => setTimeout(resolve, 0));
  }
});

View Transition yang Dibatalin

View Transitions API bikin transisi halus ala app antar halaman. Tapi cross-document view transition ngeblokir render antara screenshot halaman awal dan respons server. Waktu pengguna berinteraksi di periode blokir itu, INP-nya melonjak. Transisi ningkatin performa yang dirasain, tapi bisa ngerusak performa yang diukur kalau gak hati-hati. Rekomendasi Chrome: batalin transisi waktu pengguna berinteraksi.

1. Opt-in

Cross-document transition gak terjadi kecuali kedua dokumen opt-in lewat @view-transition di stylesheet yang dimuat semua halaman:

@view-transition {
  navigation: auto;
}

Tanpa ini, event.viewTransition selalu null dan kode pembatalan gak pernah jalan.

2. Batalin Saat Interaksi

function cancelOnInteraction(event) {
  const viewTransition = event.viewTransition;
  if (!viewTransition) return;
  ["pointerdown", "keydown"].forEach((eventType) => {
    document.addEventListener(
      eventType,
      () => {
        viewTransition.skipTransition();
      },
      { once: true }
    );
  });
}
// Outgoing page: covers the blocked rendering period.
window.addEventListener("pageswap", cancelOnInteraction);
// Incoming page: covers the transition animation itself.
window.addEventListener("pagereveal", cancelOnInteraction);

Tiga catatan: pageswap dan pagereveal nembak di window, bukan object navigation (daftar di tempat lain diam-diam gak ngapa-ngapain). Pakai keduanya: pageswap di dokumen keluar (periode blokir), pagereveal di dokumen masuk (animasinya). pointerdown buat mouse dan touch, keydown buat keyboard, { once: true } buat cleanup otomatis.

3. Selektif dan Fallback

Pakai buat: kartu produk → halaman produk, galeri gambar, ganti gambar varian, koleksi → produk. Hindari buat: tiap navigasi (terlalu agresif), submit form, checkout, halaman akun, link eksternal. Hormati prefers-reduced-motion (durasi 0.01s), dan sediain fallback: cross-document didukung Chrome/Edge 126+, Safari 18.2+, belum Firefox; same-document (document.startViewTransition()) Chrome 111+, Safari 18+, Firefox 144+:

if (document.startViewTransition) {
  // Use view transition
} else {
  // Standard navigation
  window.location = href;
}

Ini juga yang dipakai partials.apply() di 35 Developer Preview tag {% block %} dan {% partial %}.

Import Maps Tanpa Bundler

Tanpa import maps, tiap section yang pakai modul JavaScript harus ngerujuk dependensi lewat URL penuh atau path relatif. Kalau banyak section bergantung ke modul utilitas yang sama, masing-masing ngebundel salinannya sendiri atau ngandelin path yang rapuh, ngasilin kode duplikat yang nambah bobot JavaScript dan nunda main thread.

Import maps ngasih pemetaan terpusat dari nama pendek (bare specifier) ke URL file. Tiap section import dari cart-api atau utils, browser nyelesaiin ke URL aset di map. Modul di-fetch sekali, di-cache sekali, dibagi semua section. Dan waktu update modul bersama, ubah URL di satu tempat. Satu <script type="importmap"> di layout, dengan asset_url buat cache busting:

<script type="importmap">
{
  "imports": {
    "cart-api": "{{ 'cart-api.js' | asset_url }}",
    "utils": "{{ 'utils.js' | asset_url }}",
    "product-form": "{{ 'product-form.js' | asset_url }}"
  }
}
</script>

Terus di section manapun:

<script type="module">
  import { addToCart, getCart } from 'cart-api';
  import { formatMoney } from 'utils';
  import { initForm } from 'product-form';
  // Section-specific code
</script>

Jangan load es-module-shims sendiri, Shopify nyertainnya otomatis waktu browser butuh (dari 57 Performance Dasar, Metrik, dan Cara Ngukur > 5. Polyfill), salinan sendiri bikin eksekusi polyfill dobel dan konflik versi.

❌ polyfill sendiri

<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>

✅ biarin Shopify

<script type="importmap">{ "imports": { "cart-api": "{{ 'cart-api.js' | asset_url }}" } }</script>

Kapan pakai mana: import maps kalau banyak section berbagi modul dan mau manajemen dependensi terpusat tanpa build step; <script> biasa buat satu script kecil tanpa dependensi bersama; bundler (Vite, webpack) kalau butuh tree-shaking, TypeScript, atau graf dependensi kompleks (output-nya tetap bisa pakai import maps buat chunk bersama).

Lazy DOM buat Komponen Tersembunyi

Komponen yang awalnya tersembunyi (panel filter, facet, cart drawer, dialog, wishlist) tetap nambah DOM node yang browser harus perhitungkan di tiap style recalculation. Facet filter yang nambah 3.000–3.500 node ke halaman koleksi bikin tiap interaksi di halaman itu punya presentation delay lebih tinggi, walaupun panelnya tertutup. Halaman koleksi dengan 24 kartu dan panel filter penuh bisa 8.000+ node; nunda panel filter nurunin ke 4.500–5.000, ngurangin biaya recalculation di tiap klik kartu, toggle filter, dan add to cart. Ukur dengan document.querySelectorAll("*").length di console.

1. Dari template

Simpan HTML komponen di <template>: isinya ada di document fragment terpisah, nol node buat style recalculation. Suntik waktu interaksi pertama:

const placeholder = document.querySelector("#filter-panel-placeholder");
const template = document.querySelector("#filter-panel-template");
toggleButton.addEventListener("click", async () => {
  // Test `children.length`, not `hasChildNodes()`: a placeholder written across
  // multiple lines holds a whitespace text node and always has child nodes.
  if (placeholder.children.length === 0) {
    const shell = template.content.cloneNode(true);
    placeholder.appendChild(shell);
    placeholder.hidden = false;
    const sections = placeholder.querySelectorAll(".facet-group");
    // For large components (1,000+ nodes), yield between chunks. Every group is
    // initialized either way.
    const yieldBetweenGroups = sections.length > 5;
    for (const section of sections) {
      if (yieldBetweenGroups) {
        // `globalThis.` is required: a bare `scheduler?.yield()` throws a
        // ReferenceError in browsers that don't define the global at all.
        await (globalThis.scheduler?.yield?.() ??
          new Promise((r) => setTimeout(r, 0)));
      }
      initializeFacetGroup(section);
    }
  } else {
    placeholder.hidden = !placeholder.hidden;
  }
});

Komponen kecil (< 200 node) cukup sinkron; yang besar dengan sub-bagian independen, yield antar bagian biar interaksi buka pertama tetap responsif.

2. Dari Atribut data

Kalau HTML-nya dibikin server (Liquid) tapi gak boleh di live DOM awalnya, simpan sebagai string di atribut data- (gak nambah node):

{%- capture filter_panel_html -%}
  {%- for filter in collection.filters -%}
    <div class="facet-group">
      <h3>{{ filter.label }}</h3>
      {%- for value in filter.values -%}
        <label>
          <input type="checkbox" name="{{ value.param_name }}" value="{{ value.value }}">
          {{ value.label }}
        </label>
      {%- endfor -%}
    </div>
  {%- endfor -%}
{%- endcapture -%}
<div id="filter-panel-placeholder" data-html="{{ filter_panel_html | escape }}" hidden></div>
const placeholder = document.querySelector("#filter-panel-placeholder");
toggleButton.addEventListener("click", () => {
  if (placeholder.dataset.html) {
    placeholder.innerHTML = placeholder.dataset.html;
    placeholder.removeAttribute("data-html");
    placeholder.hidden = false;
  } else {
    placeholder.hidden = !placeholder.hidden;
  }
});

Guard di dataset.html, bukan hasChildNodes(): <div> yang ditulis di beberapa baris punya text node whitespace, jadi hasChildNodes() true di klik pertama dan panel gak pernah disuntik.

3. Fetch Saat Dibutuhkan

Buat section theme berbasis Liquid (cart drawer), fetch HTML-nya lewat Section Rendering API waktu interaksi pertama, pola dari bab 58. Ini cuma buat section Liquid; buat DOM yang dibikin JavaScript (facet dari layanan pihak ketiga), pakai <template> atau data-.

Trade-off yang perlu disadari: mega menu yang udah di DOM tapi tersembunyi lebih cepat di-toggle (bagian INP di atas), tapi nambah biaya tiap recalculation. Buat komponen besar yang jarang dibuka, lazy DOM menang; buat yang kecil dan sering dibuka, biarin di DOM.

Nemuin Script Terburuk

Masalah performa JavaScript gak terdistribusi merata: segelintir dependensi besar atau script pihak ketiga yang gak efisien nyumbang sebagian besar masalah. Tiga cara ngurutin buat nemuin 20% JavaScript yang nyebabin 80% masalah:

  1. Berdasarkan byte, Lighthouse Treemap: jalankan Lighthouse, buka link Treemap dari laporan, file dan modul divisualisasi sebagai kotak sebesar byte-nya. Cari script pihak ketiga yang gak dipakai, dependensi besar yang punya alternatif kecil, script dari layanan yang udah gak dipakai, dan fungsi duplikat.
  2. Berdasarkan waktu scripting, Chrome DevTools: Performance, rekam page load, tampilan Bottom-Up atau Call Tree, urutkan Self Time/Total Time, filter per domain.
  3. Berdasarkan blocking time, WebPageTest: Processing Breakdown → bagian Main Thread, script diurutkan berdasarkan total blocking time. Bisa juga blokir domain tertentu buat ngukur dampaknya. Blocking time paling langsung ngaruh ke pengalaman karena nyegah browser merespons input.

Plus Theme Check (65 Theme Check Pengenalan, Konfigurasi, Commands) yang nangkep aset remote dari domain eksternal, script yang ngeblokir parser (ParserBlockingScript), img tanpa width/height (ImgWidthAndHeight), dan bundle oversized (AssetSizeCSS default 100.000 byte mentah, AssetSizeJavaScript 10.000 byte mentah, dua-duanya ngukur ukuran mentah bukan terkompresi, dan harus diaktifin di .theme-check.yml).

Alur yang disaranin: Theme Check saat development → perbaiki → Treemap buat analisis bundle akhir → WebPageTest buat dampak nyata → verifikasi semua script masih perlu → hapus script layanan yang gak dipakai → hapus script legacy/duplikat → evaluasi sama marketing dan stakeholder apakah biaya performa sepadan manfaat fitur → cari alternatif lebih kecil buat dependensi besar. Detail audit pihak ketiga di 63 Performance Apps dan Pihak Ketiga.

Ringkasan

  1. INP = interaksi terburuk sepanjang kunjungan di p75, baik ≤ 200ms, buruk > 500ms, gak ada padanan lab (TBT proxy). Tiga fase: input delay (main thread sibuk script lain), processing (handler), presentation (style/layout/paint).
  2. Diagnosis: field data → kurangi total JavaScript → profil interaksi di Performance panel (Interactions + Main track) → fase dominan nentuin perbaikan. Masalah khas: reflow pemilih varian (batch + rAF), animasi drawer (transform), mega menu, analitik di handler klik, banyak stylesheet.
  3. defer buat hampir semua script theme (paralel, berurutan, setelah parse), async cuma pihak ketiga independen (analitik), jangan defer anti-flicker aktif dan script yang ngubah viewport awal, hapus lebih baik dari defer, load kondisional per template.name/setting.
  4. import() dinamis di listener klik/hover/focus (satu promise bersama)/IntersectionObserver/idle, { once: true }, buat chat, video, dialog.
  5. Handler: debounce (input, resize), throttle (scroll, mousemove), { passive: true } buat touch/wheel, requestAnimationFrame buat update visual, pecah long task dengan setTimeout(0)/scheduler.yield().
  6. View transition: opt-in @view-transition { navigation: auto }, batalin lewat event.viewTransition.skipTransition() di pointerdown/keydown dari pageswap dan pagereveal di window, selektif (bukan checkout/form), reduced motion, fallback (Firefox belum ndukung cross-document).
  7. Import maps di layout dengan asset_url, bare specifier di <script type="module">, modul di-fetch sekali, jangan load es-module-shims sendiri.
  8. Lazy DOM buat komponen tersembunyi besar: <template> (guard children.length, yield antar grup), atribut data-html (guard dataset.html), atau Section Rendering API; ukur querySelectorAll("*").length.
  9. Script terburuk: Treemap (byte), DevTools Bottom-Up (waktu), WebPageTest Processing Breakdown (blocking), Theme Check (ParserBlockingScript, AssetSize*), lalu hapus/ganti yang gak sepadan.

Bab selanjutnya kita ke mobile, tempat Core Web Vitals diukur secara default: tes dengan throttling CPU dan jaringan nyata, interstitial yang dihukum Google, layout responsif yang performan, tap target 48px, konten yang lebih lebar dari layar, dan paritas konten mobile-desktop.