Setelah gambar di 59 Performance Gambar dan LCP, giliran CSS dan font. CSS ngaruh ke tiga Core Web Vitals sekaligus: FCP lewat stylesheet yang ngeblokir render, CLS lewat font swap dan konten yang datang telat, dan INP lewat style recalculation mahal karena terlalu banyak stylesheet atau animasi yang ngubah layout.

Web font nambah kompleksitas lagi: bawaannya render-blocking dan bikin layout shift yang kelihatan waktu di-swap. Tiga keputusan kunci: load cuma style kritis secara sinkron, pakai size-adjust dan override metrik buat ngurangin shift waktu font swap, dan animasi pakai transform/opacity bukan properti layout.

Kurangi Jumlah Stylesheet

Tiap link stylesheet terpisah butuh parsing dan penerapan browser. URL-nya cuma di-download sekali, tapi browser harus parse dan terapin tiap stylesheet secara terpisah, yang memicu style recalculation mahal yang sangat ngerusak INP.

Di halaman koleksi dengan banyak kartu produk, tiap kartu yang ngerender stylesheet-nya sendiri bisa ngasilin 100 sampai 1.150 stylesheet terpisah, bikin style recalculation ratusan milidetik. Ini ngeblokir interaksi: buka menu, klik produk, ganti varian. Data produksi theme Trade: dari 1.150+ stylesheet (duplikat component-card.css tiap kartu, duplikat loading-spinner.css di mana-mana) ke ~30, style recalculation 98% lebih cepat, INP membaik signifikan, menu dan klik gak keblokir lagi. Recalculation terjadi sepanjang siklus halaman (parsing, eksekusi script, dan terutama interaksi), jadi recalculation cepat itu kritis buat INP.

1. Parameter skip_styles

Buat snippet yang dirender berkali-kali (kartu produk, spinner, progress bar), tambahin parameter skip_styles yang udah disinggung di 58 Performance Liquid > render Bersarang:

{%- liquid
  unless skip_styles
    echo 'component-card.css' | asset_url | stylesheet_tag
    echo 'component-price.css' | asset_url | stylesheet_tag
  endunless
-%}
<div class="card-product">
  <a href="{{ product.url }}">
    {{ product.featured_image
      | image_url: width: 600
      | image_tag: loading: image_loading
    }}
    <h3>{{ product.title }}</h3>
    <p class="price">{{ product.price | money }}</p>
  </a>
</div>
{% paginate collection.products by 12 %}
  <div class="product-grid">
    {%- assign skip_card_product_styles = false -%}
    {%- for product in collection.products limit: 12 -%}
      {%- assign image_loading = 'lazy' -%}
      {%- if forloop.index <= 4 -%}
        {%- assign image_loading = 'eager' -%}
      {%- endif -%}
      {% render 'card-product',
        product: product,
        image_loading: image_loading,
        skip_styles: skip_card_product_styles
      %}
      {%- assign skip_card_product_styles = true -%}
    {%- endfor -%}
  </div>
{% endpaginate %}

Kartu pertama ngeload style, 11 sisanya lewat: 1 stylesheet, bukan 12 (atau 1 bukan 50). Ingat: argumen render gak bisa berisi perbandingan, skip_styles: forloop.index > 1 bikin > 1 dibuang dan stylesheet gak pernah dimuat. Assign ke variabel dulu. Dan argumen loading image_tag nerima string 'eager'/'lazy' apa adanya, boolean atau angka ngasilin loading="true" yang gak valid.

2. Style Global ke base.css

Buat snippet yang dipakai di seluruh theme (spinner, progress bar, sistem ikon, komponen UI umum), pindahin CSS-nya ke base.css dan hapus link stylesheet dari snippet-nya.

3. Konsolidasi File Terkait

Daripada product-card.css, product-price.css, product-badge.css, product-rating.css terpisah, gabung jadi satu component-product.css. Manfaatnya: kompresi lebih baik (gzip/Brotli lebih efektif di file besar), lebih sedikit request, dan parsing lebih cepat (satu file besar lebih cepat dari banyak file kecil).

CLS dari Font Swap

Dengan font-display: swap, browser langsung ngerender teks pakai font sistem cadangan (Arial, Times) sambil web font di-download, terus swap setelah datang (biasanya 100–500ms setelah paint pertama). Kalau web font punya lebar karakter, tinggi baris, atau spasi berbeda dari cadangan, semua teks reflow sekaligus, ngegeser konten sekitarnya. Di halaman padat teks, ini bisa 0,05–0,15 CLS.

Catatan penting: filter font_face dari 19 Filters HTML, URL, Media, Color, dan Font > 1. font_face gak nambahin font-display kecuali kamu kasih eksplisit. Tanpa itu, browser default ke auto yang di Chrome berperilaku kaya block: teks gak kelihatan sampai 3 detik (FOIT). Jadi selalu font_display: 'swap'.

Ini teknik lanjutan, dipakai setelah penyebab CLS yang lebih umum (gambar tanpa dimensi, konten app tanpa ruang) beres, dan kalau RUM nunjukin font swap penyumbang utama CLS. Gak perlu sama sekali kalau theme pakai system font.

1. line-height Eksplisit Dulu

Sebelum ngukur metrik font, set line-height tanpa satuan. Nilai tanpa satuan dihitung sebagai kelipatan font-size, jadi tinggi kotak baris gak bergantung metrik font dan swap gak bisa ngubahnya:

body {
  line-height: 1.5;
}

Dengan line-height: normal, kotak baris dari ascent, descent, dan line gap font, jadi swap antara dua font dengan metrik beda ngubah tinggi tiap baris, dan bedanya proporsional font-size (heading geser lebih banyak). Ini cuma nyelesaiin setengah vertikal. Lebar glyph yang beda tetap ngubah di mana baris wrap, ngubah jumlah baris, ngegeser konten di bawah. size-adjust buat lebar, line-height buat tinggi.

2. Samain Metrik Cadangan

Descriptor @font-face buat font cadangan:

  1. size-adjust: nyekala glyph cadangan.
  2. ascent-override: tinggi di atas baseline.
  3. descent-override: kedalaman di bawah baseline.
  4. line-gap-override: spasi baris.

Pasangan umum: Futura → Trebuchet MS, Helvetica Neue → Arial (metrik hampir identik), Playfair Display → Georgia, Montserrat → Verdana, Lato → Tahoma. Tapi gak ada nilai universal: nilai yang tepat tergantung pasangan font, bobot, dan style, jadi harus diukur per pasangan:

  1. Ambil unitsPerEm, ascender, descender, lineGap, dan x-height/lebar rata-rata karakter dari kedua font (lewat Fontaine yang bisa ngegenerate rule cadangannya, atau Capsize).
  2. size-adjust = rasio lebar rata-rata karakter kedua font, dalam persen.
  3. Tiga override = ascender/descender/lineGap web font ÷ unitsPerEm ÷ size-adjust, dalam persen.
  4. Verifikasi: render blok teks yang sama dengan cadangan dan web font, bandingin tinggi dan lebar di panel Elements sampai cocok.
/* In your main web font declaration, make sure font-display is swap */
@font-face {
  font-family: "Brand Font";
  src: url("/fonts/brand-font.woff2") format("woff2");
  font-display: swap;
}
/* Create a customized fallback font. Replace these values with ones
   measured for your own web font and fallback pair. */
@font-face {
  font-family: "Brand Font Fallback";
  src: local("Arial");
  size-adjust: 107%;
  ascent-override: 92%;
  descent-override: 24%;
  line-gap-override: 0%;
}
body {
  font-family: "Brand Font", "Brand Font Fallback", sans-serif;
}

Satu catatan: file font lebih kecil mempersingkat jendela sebelum swap, tapi gak ngubah CLS yang swap sebabkan, karena shift-nya dari ketidakcocokan metrik, bukan waktu download. Font kustom di assets/ dikirim apa adanya, jadi subset sendiri ke karakter yang toko pakai sebelum upload.

Critical CSS Sinkron

CSS itu render-blocking by design, buat nyegah Flash of Unstyled Content (FOUC). Waktu CSS async dimuat buat konten di viewport awal: konten render tanpa style, style datang telat dan bikin re-render, konten bergeser (CLS naik), FOUC bikin bingung, dan bisa memicu load gambar yang gak perlu karena IntersectionObserver kebingungan.

Tujuannya bukan ngilangin semua CSS render-blocking, tapi strategis soal apa yang ngeblokir. Critical CSS = set minimum style buat ngerender konten di viewport awal dengan bener, dan itu harus selalu render-blocking. Aturannya: kalau stylesheet-nya nge-style apapun yang pengguna lihat sebelum scroll, load sinkron. Defer cuma style non-kritis buat konten di luar viewport awal.

1. Pola Async CSS

<link
  rel="stylesheet"
  href="style.css"
  media="print"
  onload="this.media='all'"
/>
<noscript><link rel="stylesheet" href="style.css" /></noscript>

Cara kerjanya: browser lihat media="print" → prioritas download Lowest → render awal gak pakai style ini → setelah load, JavaScript ngubah media ke all → prioritas jadi Highest → halaman re-render dengan style baru. <noscript> buat yang JavaScript-nya mati.

2. Dengan section.index

{% unless section.index > 3 %}
  {%- comment -%} Initial viewport, and any context where section.index is nil: render-blocking {%- endcomment -%}
  <link rel="stylesheet" href="{{ 'section.css' | asset_url }}">
{% else %}
  {%- comment -%} Outside the initial viewport: async {%- endcomment -%}
  <link rel="stylesheet" href="{{ 'section.css' | asset_url }}" media="print" onload="this.media='all'">
  <noscript><link rel="stylesheet" href="{{ 'section.css' | asset_url }}"></noscript>
{% endunless %}

Tes async sebagai section.index > 3 positif, biar nil (editor, statis, Section Rendering API) tetap sinkron. Kalau ragu section-nya aman di-async atau gak, pendekatan konservatif: taruh <link rel="stylesheet"> tepat sebelum konten section, browser bisa render semua di atasnya sambil preload scanner ngambil stylesheet-nya.

3. Kasus Nyata Dawn

Dawn pernah nerapin async CSS ke komponen yang kelihatan di viewport awal. Waktu ketemu di PR #2270, itu bikin layout shift yang kelihatan: heading “Obsessive Attention. Intelligent Effort.” bergeser ke kanan karena component-rte.css async. Ngehapus async CSS dari konten viewport awal ningkatin CLS 95% (0,065 ke hampir nol). Pola async-nya diterapin di terlalu banyak tempat, terutama terasa di kunjungan pertama dan koneksi lambat.

Pakai async: section 4+, style non-kritis, setelah tes membuktikan bermanfaat. Jangan: tiga section pertama, konten viewport awal, style kritis, tanpa tes sebelum/sesudah. Polanya bisa lebih merugikan dari menguntungkan, selalu verifikasi dengan Performance > Insights (FCP, LCP, CLS, filmstrip) dan RUM setelah deploy.

Self-host Font di CDN Shopify

Kalau typeface gak ada di font library Shopify (31 Dynamic Sources dan Fonts), self-host di CDN Shopify daripada load dari Google Fonts atau layanan eksternal. Ini ngilangin koneksi ke domain eksternal (DNS, TCP, TLS), pakai CDN Shopify otomatis, cache busting lewat asset_url, discovery lebih cepat, jalan dengan Early Hints, dan ngurangin satu dependensi eksternal. Urutannya: self-hosted (tercepat) > Google Fonts teroptimasi dengan preconnect dan async > Google Fonts biasa (paling lambat, banyak koneksi terpisah).

Kapan self-host: font gak ada di library, butuh subset/variable axis/Unicode range yang file library gak nyakup, atau typeface dikunci brand guideline jadi gak boleh jadi setting merchant. Kalau gak ada yang berlaku, filter font Shopify lebih rendah perawatannya dengan hasil yang sama.

1. Langkahnya

  1. Download lewat Google Webfonts Helper: WOFF2 utama (kompresi terbaik) dan WOFF cadangan. Lewati EOT, TTF, SVG.
  2. Taruh di /assets: montserrat-v31-latin-regular.woff2, .woff, -700.woff2, -700.woff.
  3. @font-face inline di <head> theme.liquid pakai asset_url (ditemukan segera, lebih cepat dari CSS eksternal):
<style>
  /* montserrat-regular - latin */
  @font-face {
    font-display: swap;
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 400;
    src: url('{{ "montserrat-v31-latin-regular.woff2" | asset_url }}') format('woff2'),
         url('{{ "montserrat-v31-latin-regular.woff" | asset_url }}') format('woff');
  }
  /* montserrat-700 - latin */
  @font-face {
    font-display: swap;
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 700;
    src: url('{{ "montserrat-v31-latin-700.woff2" | asset_url }}') format('woff2'),
         url('{{ "montserrat-v31-latin-700.woff" | asset_url }}') format('woff');
  }
</style>
  1. Pakai dengan fallback kalau web font gagal: font-family: "Montserrat", system-ui, -apple-system, sans-serif;.
  2. Preload font buat konten viewport awal:
{{ 'montserrat-v31-latin-regular.woff2' | asset_url | preload_tag: as: 'font', type: 'font/woff2' }}

Preload font harus CORS-enabled walaupun same-origin. Jangan kasih crossorigin ke preload_tag, dia ngeluarin crossorigin="anonymous" sendiri waktu as itu font. Alternatif: assets/fonts.css.liquid kalau lebih suka di file CSS.

System Font

Tiap file web font bikin dependensi render: parse HTML dan temukan @font-face → request file → tunggu download → terapin. Di koneksi cepat puluhan ms, di mobile lambat ratusan ms, dengan FOIT atau FOUT plus CLS dari swap. System font udah terpasang di perangkat: teks render langsung, tanpa request, tanpa swap, tanpa CLS font.

Pilih system font kalau: performa prioritas utama, brand gak punya identitas tipografi khas, target mobile dengan koneksi terbatas, mau ngilangin semua CLS font. Web font pantas kalau brand butuh typeface tertentu, biayanya udah diukur dan diterima, dan kamu udah nerapin self-hosting dan tuning fallback.

1. Stack di CSS

body {
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
}

San Francisco di Apple, Segoe UI di Windows, default sans-serif di Linux/Android, semua tanpa request.

2. Di Theme Setting

font_picker wajib default berupa handle font nyata, dan system bukan handle. Handle system font dari 31 Dynamic Sources dan Fonts > 2. System Fonts: system_ui_n4/i4/n7/i7, sans_serif_n4 dan varian, serif, mono (dua terakhir tanpa sufiks karena satu varian):

{
  "name": "Typography",
  "settings": [
    {
      "type": "font_picker",
      "id": "type_body_font",
      "label": "Body font",
      "default": "system_ui_n4"
    },
    {
      "type": "font_picker",
      "id": "type_header_font",
      "label": "Heading font",
      "default": "system_ui_n4"
    }
  ]
}

Di layout, cek font.system? buat output stack system font, bukan web font:

{%- assign body_font = settings.type_body_font -%}
{% style %}
  {{ body_font | font_face: font_display: 'swap' }}
  :root {
    {%- if body_font.system? -%}
      --font-body-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
        Roboto, "Helvetica Neue", Arial, sans-serif;
    {%- else -%}
      --font-body-family: {{ body_font.family }}, {{ body_font.fallback_families }};
    {%- endif -%}
  }
{% endstyle %}

Tiga catatan:

  1. font_face ngeluarin deklarasi CSS, bukan tag HTML, jadi harus di dalam stylesheet. Tag {% style %} ngebungkusnya dengan <style>. Di luar stylesheet, deklarasinya dicetak sebagai teks dan font gak pernah terdaftar.
  2. Waktu font-nya system, font_face ngembaliin string kosong (gak ada file di CDN), jadi panggilannya gak butuh guard system?.
  3. Guard preload dan preconnect dengan unless font.system?: font_url ngembaliin string kosong buat system font, jadi preload tanpa guard ngeluarin tag dengan href kosong. Dawn ngelakuin ini: font_face tanpa syarat di {% style %}, tapi preconnect ke fonts.shopifycdn.com dan tiap preload dibungkus {%- unless ... .system? -%}, jadi waktu merchant milih system font, gak ada request font sama sekali.

Ruang buat Konten App

Urutan tipikal: browser render halaman awal dari HTML theme → JavaScript app load dan jalan → script nyuntik konten baru ke DOM → browser reflow dan ngegeser konten yang ada. Shift ini tercatat sebagai CLS dan bikin pembeli kehilangan posisi atau salah klik. Yang paling sering: widget rating/review di bawah judul produk, baris trust badge/ikon pembayaran, widget estimasi pengiriman.

Cookie banner dan chat bubble datang dari app embed block yang disuntik sebelum </head> atau </body>, bukan ke section, jadi CSS section gak nyampe. Mereka ngegeser layout cuma kalau render di normal flow, jadi perbaikannya position: fixed, bukan ruang cadangan.

1. Caranya

App modern pakai app block (28 Blocks) yang bisa ditarget CSS buat mereservasi ruang sebelum kontennya datang. Izinkan @app di schema section, terus di Custom CSS section (dari theme editor, setting yang dikontrol platform dari 32 Config settings_schema.json, settings_data.json, markets.json) target .shopify-app-block:

/* Reserve space for app blocks inside the product info column */
.product__info-container .shopify-app-block {
  min-height: 48px;
}

Shopify ngebungkus tiap block dengan class shopify-block, dan nambahin shopify-app-block buat yang dari app. Target .shopify-app-block biar cuma konten app yang dapat ruang, bukan block theme sendiri. Scope ke section/wadah yang punya block-nya, karena min-height di semua app block halaman mereservasi ruang di tempat yang gak butuh. Wrapper-nya juga punya id="shopify-block-<id>" buat nargetin satu block spesifik.

Nilai min-height per tipe: rating bintang dan badge kecil 24–30px, ringkasan review dan baris trust badge 48–60px, widget review penuh 80–200px. Cara ngukurnya: install app di dev store, inspect widget-nya di DevTools, catat offsetHeight wadahnya, pakai nilai itu (atau sedikit lebih), dan tes di mobile karena tingginya sering beda:

/* Every app block in the product info column */
.product__info-container .shopify-app-block {
  min-height: 28px;
}
/* The rating block specifically, which wraps on small screens */
#shopify-block-1a2b3c4d {
  min-height: 28px;
}
@media screen and (max-width: 749px) {
  #shopify-block-1a2b3c4d {
    min-height: 56px;
  }
}

Pasangkan dengan placeholder biar celahnya kebaca sebagai konten yang loading, bukan bug (:empty cuma cocok wrapper tanpa child node sama sekali, termasuk whitespace, jadi konfirmasi di DevTools):

#shopify-block-1a2b3c4d:empty {
  background: linear-gradient(90deg, #f4f4f4 25%, #ebebeb 50%, #f4f4f4 75%);
  border-radius: 4px;
}

2. Alternatif Desain

  1. Posisiin konten app di samping konten statis yang sama tinggi atau lebih: shift tetap terjadi, tapi gak ada yang kelihatan bergerak.
  2. Taruh di bawah section atau di luar viewport awal: shift di bawah viewport gak dihitung CLS.
  3. Skeleton (kotak abu-abu, shimmer) yang cocok dimensi widget.

Pola min-height berlaku buat wadah framework apapun yang ngerender konten dengan JavaScript di dalam theme Liquid.

Animasi: transform dan opacity

Nganimasiin properti layout (top, left, right, bottom, width, height) itu anti-pola: browser nganggap tiap frame animasi sebagai layout shift, ngasilin CLS masif, animasi patah-patah dan scroll lag karena layout dihitung ulang tiap frame, INP buruk karena kerjanya di main thread, dan gak hardware-accelerated.

Animasiin cuma dua properti: transform dan opacity. Compositor thread yang nanganin, gak memicu layout atau paint: CLS 0, hardware acceleration, main thread bebas.

/* Anti-pattern: creates a layout shift on every frame */
@keyframes slideUp {
  from {
    bottom: -100%;
  }
  to {
    bottom: 0;
  }
}
/* Recommended: hardware-accelerated, no layout shift */
@keyframes slideUp {
  from {
    transform: translateY(100%);
  }
  to {
    transform: translateY(0);
  }
}

Kasus umum: cookie banner yang slide dari bawah pakai bottom. Secara visual gak ngegeser konten, tapi tetap tercatat layout shift karena pakai properti posisi. Ganti ke transform. Dan selalu hormati preferensi reduced motion:

@media (prefers-reduced-motion: reduce) {
  .animated-element {
    animation: none;
    transition: none;
  }
}

Satu Menu Responsif

DOM node berlebih ngelambatin parsing HTML dan kalkulasi style, ngelambatin render awal (FCP), nambah memori, dan ngelambatin manipulasi DOM (INP). Pelaku paling umum: menu duplikat buat mobile dan desktop, satu disembunyiin CSS, satu ditampilin, DOM dua kali lipat. Navigasi dengan 500–2.000 node itu umum di theme begini, dan ngurangin setengahnya langsung ngurangin parsing, style, dan latensi interaksi.

1. Satu Menu dengan CSS Responsif

Render link sekali, variasikan presentasi dengan media query:

<nav class="site-nav" aria-label="Primary">
  <button class="site-nav__toggle" aria-expanded="false" aria-controls="site-nav-menu">
    Menu
  </button>
  <ul id="site-nav-menu" class="site-nav__menu">
    {%- for link in linklists.main-menu.links -%}
      <li class="site-nav__item">
        <a href="{{ link.url }}" class="site-nav__link">{{ link.title }}</a>
      </li>
    {%- endfor -%}
  </ul>
</nav>
/* Mobile first: hidden drawer toggled by the menu button. */
.site-nav__menu {
  display: none;
  flex-direction: column;
  gap: 0.5rem;
}
.site-nav__toggle[aria-expanded='true'] + .site-nav__menu {
  display: flex;
}
/* Desktop: horizontal bar, toggle button hidden. */
@media (min-width: 750px) {
  .site-nav__toggle {
    display: none;
  }
  .site-nav__menu {
    display: flex;
    flex-direction: row;
    gap: 1.5rem;
  }
}

Markup yang sama jadi drawer di mobile dan bar horizontal di desktop, tombol toggle ngatur aria-expanded biar tetap aksesibel (55 Accessibility). Ini juga saran dari 36 Navigasi dan Menu > Menu Mobile.

2. Hybrid Rendering buat Mega Menu

Buat mega menu kompleks yang gak bisa disederhanain: render bagian yang kelihatan dengan Liquid (link tingkat atas, langsung tersedia dan ditemukan mesin pencari), fetch bagian tersembunyi dengan JavaScript (submenu diambil lewat Section Rendering API waktu hover/klik), dan sediain fallback (link ke halaman koleksi buat bot dan pengguna tanpa JavaScript). Taruh mega menu penuh di section sendiri, header cuma ngerender link ringan plus <div id="mega-nav-submenu" hidden></div>, dan waktu hover, fetch section-nya sekali dan suntik. Karena Liquid submenu yang kompleks gak jalan di load awal, ukuran HTML dan biaya parse turun.

Stylesheet Subsetting

Shopify otomatis nge-subset CSS dari tag {% stylesheet %} (dari 54 Best Practices Umum > Tag javascript dan stylesheet) biar tiap halaman cuma ngeload style yang dibutuhin. Daripada ngebundel semua CSS {% stylesheet %} dari tiap file ke satu payload, Shopify ngenalin file mana yang ada di render tree halaman (section, block, snippet yang beneran dirender) dan cuma nyertain CSS mereka. Kalau sections/collection.liquid ngerender snippets/product-card.liquid, CSS keduanya masuk di halaman yang ngerender section koleksi, karena induknya di render tree.

Yang gak kena subsetting: stylesheet aset (asset_url | stylesheet_tag), atribut style inline, tag <style> HTML, dan CSS di tag {% style %} Liquid.

1. Pola Kompatibel dan Gak

Theme kompatibel kalau class CSS tiap file cuma dipakai di file itu atau file yang dia render langsung. Theme dengan pola co-located, component-scoped biasanya udah kompatibel.

Gak kompatibel: class yang didefinisiin di {% stylesheet %} satu file tapi dipakai di file lain yang gak berhubungan. Kalau file pendefinisinya gak ada di render tree halaman, style-nya gak disertakan:

❌ footer bergantung ke stylesheet header

{%- comment -%} sections/header.liquid {%- endcomment -%}
{% stylesheet %}
  .site-banner { background: var(--color-accent); padding: 0.5rem 1rem; }
{% endstylesheet %}
{%- comment -%} sections/footer.liquid, pakai .site-banner tanpa definisi {%- endcomment -%}
<div class="site-banner">{{ section.settings.promo_text }}</div>

✅ class global di aset yang selalu dimuat

{%- comment -%} assets/global.css: .site-banner { ... } {%- endcomment -%}
{%- comment -%} layout/theme.liquid {%- endcomment -%}
{{ 'global.css' | asset_url | stylesheet_tag }}

2. Ngecek dan Memperbaiki

Theme Check ValidScopedCSSClass (aktif di konfigurasi recommended, 66 Theme Check Referensi Aturan A–L) ngedeteksi dependensi CSS lintas file: “CSS class site-banner is defined in another liquid file’s stylesheet tags that isn’t an explicit ancestor: sections/header.liquid”. Class di assets/*.css di-allowlist. Perbaikannya: pindahin CSS ke file yang makainya (atau ke induk bersama), atau buat class yang beneran global (utilitas, tipografi), pindahin ke .css di assets/ yang dimuat lewat stylesheet_tag di layout.

3. Dengan Section Rendering API

Subsetting juga berlaku waktu section di-fetch dinamis: responsnya nyertain <style data-section-stylesheet> berisi CSS subset section itu:

<section id="shopify-section-sections--28375442096184__featured_collection" class="shopify-section">
  <style data-section-stylesheet>
    .collection-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
  </style>
  <!-- section content -->
</section>

Kalau kamu nyisipin seluruh respons section ke DOM, style-nya ikut otomatis. Kalau cuma sebagian (kaya pola innerHTML bagian dalam di bab-bab sebelumnya), kamu juga harus ngekstrak dan nyisipin elemen <style data-section-stylesheet> biar style-nya berlaku.

Ringkasan

  1. Tiap stylesheet terpisah = style recalculation yang ngeblokir INP (1.150 → 30 stylesheet = 98% lebih cepat). skip_styles lewat variabel di snippet berulang, style global ke base.css, konsolidasi file terkait.
  2. Font swap → CLS: font_face butuh font_display: 'swap' eksplisit (default block/FOIT), line-height tanpa satuan dulu, terus cadangan dengan size-adjust/ascent-override/descent-override/line-gap-override yang diukur per pasangan (Fontaine/Capsize). Teknik lanjutan, gak perlu buat system font.
  3. Critical CSS sinkron buat apapun yang kelihatan sebelum scroll; pola async (media="print" + onload + noscript) cuma section 4+ (unless section.index > 3), setelah tes, karena bisa memperburuk (Dawn: CLS turun 95% setelah ngehapus async yang salah tempat).
  4. Self-host font di /assets (WOFF2 + WOFF), @font-face inline di <head> dengan asset_url dan font-display: swap, preload_tag: as: 'font' tanpa crossorigin manual; cuma kalau font gak ada di library atau dikunci brand.
  5. System font: tanpa request/swap/CLS, stack system-ui, -apple-system, ..., default font_picker system_ui_n4, cek font.system? buat stack CSS, font_face di dalam {% style %} (kosong buat system), guard preload/preconnect dengan unless system?.
  6. Ruang buat app block: min-height di .shopify-app-block yang di-scope ke wadah atau #shopify-block-<id>, ukur offsetHeight di desktop dan mobile, placeholder :empty; embed block (cookie, chat) pakai position: fixed.
  7. Animasi cuma transform dan opacity, properti posisi bikin CLS tiap frame (termasuk cookie banner), hormati prefers-reduced-motion.
  8. Satu menu responsif dengan media query dan aria-expanded, atau hybrid: tingkat atas di Liquid, submenu via Section Rendering API, fallback buat bot.
  9. Stylesheet subsetting: {% stylesheet %} cuma dimuat buat file di render tree, class harus dipakai di file sendiri atau turunan langsung (ValidScopedCSSClass), class global ke assets/*.css, dan sertakan <style data-section-stylesheet> waktu nyuntik sebagian respons Section Rendering API.

Bab selanjutnya kita ke JavaScript: defer/async, load saat interaksi dengan import(), debounce/throttle/passive listener, view transition, import maps, lazy DOM buat komponen tersembunyi, memahami tiga fase INP, dan nemuin script terburuk lewat Treemap dan Coverage.