Gambar biasanya jadi elemen LCP, metrik yang ngukur kapan konten terbesar yang kelihatan selesai dimuat, dan yang paling dekat sama kecepatan yang dirasain pembeli. Strategi loading gambar yang bener (eager vs lazy,
fetchpriority, ukuran responsif) langsung ngaruh ke LCP hampir semua storefront.
Kabar baiknya, filter image_url dan image_tag dari 19 Filters HTML, URL, Media, Color, dan Font > Filter Gambar udah ngurus bagian paling rumit: srcset, width/height buat nyegah layout shift, dan CDN. Pakai keduanya konsisten itu baseline-nya. Bab ini soal keputusan dan pola di luar filter itu sendiri. Tiga rekomendasi pertama masuk daftar esensial Shopify (dampak High).
fetchpriority=“high” di Gambar LCP
Bawaannya semua gambar ditemukan dengan prioritas Low. Setelah HTML awal di-parse dan layout selesai, browser baru tau gambar mana yang di viewport dan naikin prioritasnya ke High. Ada jeda antara kapan browser bisa mulai download gambar kritis dan kapan dia beneran mulai. fetchpriority="high" ngilangin jeda itu buat gambar LCP.
Aturannya:
- Cuma di gambar LCP, satu per halaman. Kamu harus yakin itu elemen LCP: kelihatan di viewport awal, gambar hero atau gambar produk utama, dan sebaiknya di section pertama.
- Gabungkan dengan
loading="eager"atau tanpaloading. Jangan pernah denganloading="lazy". - Berlebihan bikin lebih lambat:
fetchpriority="high"di banyak gambar ngeganggu heuristik prioritas browser sendiri. Pakai cuma kalau kamu punya info soal pentingnya elemen yang browser belum tau.
Pola lengkap berdasarkan posisi section, dengan aturan nil dari 58 Performance Liquid > section.index: Atas vs Bawah Lipatan:
{% if section.index == 1 %}
{%- comment -%} First section: highest priority {%- endcomment -%}
{{ product.featured_image
| image_url: width: 1000
| image_tag:
loading: 'eager',
fetchpriority: 'high',
widths: '400, 600, 800, 1000',
sizes: '(min-width: 1000px) 900px, calc(100vw - 2rem)'
}}
{% elsif section.index > 3 %}
{%- comment -%} Fourth section and later: lazy load {%- endcomment -%}
{{ product.featured_image
| image_url: width: 1000
| image_tag:
loading: 'lazy',
widths: '400, 600, 800, 1000',
sizes: '(min-width: 1000px) 900px, calc(100vw - 2rem)'
}}
{% else %}
{%- comment -%} Second and third sections, and any context where section.index is nil: eager, normal priority {%- endcomment -%}
{{ product.featured_image
| image_url: width: 1000
| image_tag:
loading: 'eager',
widths: '400, 600, 800, 1000',
sizes: '(min-width: 1000px) 900px, calc(100vw - 2rem)'
}}
{% endif %}Cek di DevTools: Performance > Insights buat nemuin elemen LCP dan resource load delay-nya, dan kolom Priority di Network panel buat mastiin gambar LCP-nya High.
Jangan Lazy-load Gambar LCP
Ini anti-pola paling umum dan paling merusak di theme Shopify. Render sisi server dengan image_tag ngasih browser waktu tunggu maksimal buat gambar LCP, tapi cuma kalau dimuat eager.
Waktu Liquid ngerender <img> dengan src ke HTML, preload scanner browser nemuin URL gambar waktu parse HTML, sebelum CSS di-parse, sebelum JavaScript jalan, sebelum layout mulai. Itu head start yang sangat berharga. Dua anti-pola yang ngerusaknya:
loading="lazy"di kandidat LCP: nurunin prioritasnya dan nunda download sampai setelah browser memproses eventIntersectionObservertersembunyi.- Library lazy-load JavaScript (lazysizes, lozad, vanilla-lazyload) yang ngeganti
srcdengandata-srcnon-standar, yang nyembunyiin URL dari preload scanner sepenuhnya sampai JavaScript jalan.
Dua-duanya umum, dua-duanya ngebatalin manfaat preload scanner.
1. Caranya
- Ganti
loading="lazy"jadiloading="eager"atau hapus atributnya (eager itu default browser). - Buang library lazy-load: ganti semua
data-srcjadisrc, tambahinloading="lazy"ke gambar di luar viewport awal, hapus tag script library-nya. Browser modern punya lazy-load native. - Andalkan default
image_tag: tanpa argumenloading, dia otomatis eager buat section 1–3 dan lazy buat 4+, dan eager kapanpunsection.index0nil. Kalau override manual, ikuti penanganannilyang sama (tes lazy sebagaisection.index > 3).
❌ library lazy-load, URL tersembunyi dari preload scanner
<img data-src="{{ image | image_url: width: 1000 }}" class="lazyload">✅ native
{{ image | image_url: width: 1000 | image_tag: loading: 'lazy' }}2. forloop.index buat Section Padat Gambar
Section koleksi bisa ngerender 50 produk sekaligus, semuanya di section pertama. section.index aja gak cukup, karena cuma beberapa gambar pertama yang kelihatan dan jadi kandidat LCP. Pakai forloop.index buat eager di kartu-kartu itu aja:
{% for product in collection.products %}
{%- liquid
if forloop.index <= 4
assign image_loading = 'eager'
else
assign image_loading = 'lazy'
endif
-%}
{{ product.featured_image
| image_url: width: 400
| image_tag:
loading: image_loading,
widths: '200, 300, 400',
sizes: '(min-width: 1200px) calc(25vw - 2rem), (min-width: 768px) calc(33vw - 2rem), calc(50vw - 2rem)'
}}
{% endfor %}forloop.index gak pernah nil, jadi <= aman di sini.
3. Hindari Auto-sizes di Gambar LCP
Ngitung sizes pakai JavaScript (yang dilakuin beberapa library lazy-load) nunda download karena browser gak bisa milih source yang tepat sampai JavaScript jalan. Selalu kasih sizes eksplisit buat gambar di viewport awal.
4. Contoh Hero
{%- comment -%} Before: always lazy loads, even when this section is at the top of the page {%- endcomment -%}
{{ section.settings.image
| image_url: width: 1600
| image_tag:
loading: 'lazy',
widths: '600, 900, 1200, 1600',
sizes: '100vw'
}}
{%- comment -%} After: section.index determines loading at render time, and nil stays eager {%- endcomment -%}
{%- liquid
assign hero_loading = 'eager'
assign hero_priority = 'auto'
if section.index == 1
assign hero_priority = 'high'
elsif section.index > 3
assign hero_loading = 'lazy'
endif
-%}
{{ section.settings.image
| image_url: width: 1600
| image_tag:
loading: hero_loading,
fetchpriority: hero_priority,
widths: '600, 900, 1200, 1600',
sizes: '100vw'
}}Perhatiin: default-nya eager dan auto, cuma index 1 yang high dan cuma index > 3 yang lazy. nil gak masuk ke kedua cabang, jadi tetap eager.
Jangan Sembunyiin Gambar LCP di Balik Animasi
Chrome ngecualiin elemen dengan opacity: 0 dari kandidat LCP karena gak ngasih nilai visual. Kalau CSS ngeset opacity: 0 awal dengan transisi ke 1, browser nunggu sampai elemen repaint dengan opasitas bukan nol sebelum nyatat LCP. Gambar bisa selesai download di 2 detik, tapi kalau animasi JavaScript baru nge-fade-in di 6 detik, LCP tercatat 6 detik.
Pola yang bikin masalah:
- Animasi transisi halaman di theme setting yang nyembunyiin semua konten sampai efeknya main. Ini masalah animasi paling berdampak dan paling gampang diperbaiki.
- Class reveal fade-in (
.is-loading,.animated-reveal) yang ngesetopacity: 0di gambar dan nunggu JavaScript ngehapusnya setelahDOMContentLoaded. - Animasi hero yang nganimasiin opasitas heading, logo, gambar waktu masuk viewport: 650–900ms LCP terbuang.
- Library Animate On Scroll yang di-hardcode di hero atau diterapin seluruh situs.
display: noneyang di-toggle JavaScript, yang lebih buruk dari opasitas: ngehapus elemen dari layout sepenuhnya, jadi waktu di-setblock, browser harus ngitung ulang layout elemen dan sekitarnya, LCP telat plus lonjakan CLS. Dan download-nya tetap terjadi (eager<img>tetap fetch walaudisplay: none), jadi bayar bandwidth tanpa dapat kredit LCP. Kebalikannya buat gambar lazy: browser gak ngeloaddisplay: none+loading="lazy", tapi ngeloadopacity: 0.
Di studi kasus nyata, ngehapus fade-in dari gambar LCP ningkatin LCP 3–6 detik. Satu toko: LCP mobile dari 3,6s ke 2,2s setelah ngehapus fade-in hero, dengan add-to-cart naik 9,4%.
1. Caranya
- Matiin animasi transisi halaman dulu: cek theme setting Animations buat “Animate between pages”/“Page transitions”. Satu toggle ini udah benerin Core Web Vitals banyak toko tanpa ngubah kode.
- Hapus animasi fade-in dari elemen LCP (hero image, gambar produk utama, heading hero). Kalau animasi buat konten bawah, pastiin elemen LCP dikecualikan.
- Kalau harus ada transisi: pakai CSS
@keyframes(mulai di render pertama, opasitas naik di atas 0 di frame animasi pertama, jadi langsung jadi kandidat LCP tanpa nunggu script), durasi di bawah 0,3s, dan buat transisi seluruh halaman mulai dari eventpagerevealyang nembak sebelum render pertama dokumen baru.
❌ fetchpriority dan CSS saling membatalkan
{{ product | image_url: width: 1000 | image_tag: class: 'animate-reveal', fetchpriority: 'high' }}.animate-reveal { opacity: 0; transform: translateY(20px); transition: opacity 0.6s ease, transform 0.6s ease; }
.animate-reveal.visible { opacity: 1; transform: translateY(0); }✅ kecualikan section pertama (tes positif biar nil aman)
{% if section.index > 1 %}
{%- comment -%} Sections 2 and later: safe to animate {%- endcomment -%}
{{ product
| image_url: width: 1000
| image_tag: class: 'animate-reveal'
}}
{% else %}
{%- comment -%} First section, and any context where section.index is nil {%- endcomment -%}
{{ product
| image_url: width: 1000
| image_tag: fetchpriority: 'high'
}}
{% endif %}Tes kasus animasi secara positif dengan section.index > 1, bukan kasus pengecualian dengan == 1, karena nil bakal masuk cabang animasi dan ngembaliin masalahnya.
Jangan Pakai CSS background-image buat Hero
Background image CSS ditemukan telat dalam rantai: HTML download dan parse → CSS ditemukan dan download → CSS parse → URL background ditemukan → gambar baru mulai download. Dengan <img>, preload scanner nemuin gambar waktu parse HTML awal, download mulai paralel sama CSS. Dampaknya makin besar kalau background-nya di CSS eksternal.
Manfaat tambahan <img>: bisa fetchpriority="high", bisa preload, srcset/sizes lebih baik, gampang nambah width/height buat CLS, dan jalan sama image_tag. Background image boleh buat: pola dekoratif, elemen non-LCP, di luar viewport awal, elemen UI kecil, dan data URI/SVG inline (gak ada request).
Caranya: <img> dengan posisi absolut buat efek visual yang sama:
<section class="hero">
{% unless section.index > 2 %}
{{ section.settings.image
| image_url: width: 2000
| image_tag:
loading: 'eager',
fetchpriority: 'high',
widths: '600, 900, 1200, 1600, 2000',
sizes: '100vw',
class: 'hero__image'
}}
{% else %}
{{ section.settings.image
| image_url: width: 2000
| image_tag:
loading: 'lazy',
widths: '600, 900, 1200, 1600, 2000',
sizes: '100vw',
class: 'hero__image'
}}
{% endunless %}
<div class="hero__content">
<h1>{{ section.settings.title }}</h1>
</div>
</section>.hero {
position: relative;
min-height: 400px;
}
.hero__image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.hero__content {
position: relative;
z-index: 1;
}Gambarnya gak butuh z-index karena .hero__content udah di atasnya. Jangan kasih z-index: -1: position: relative aja gak bikin .hero jadi stacking context, jadi -1 lolos dan bisa ngegambar di belakang latar section atau wrapper, ninggalin elemen LCP kosong. Dawn dan Horizon dua-duanya posisiin hero media begini.
❌ inline background, ditemukan telat
<section class="hero" style="background-image: url({{ section.settings.image | image_url: width: 2000 }});">
<h1>{{ section.settings.title }}</h1>
</section>✅ <img> absolut, ditemukan preload scanner
<section class="hero">
{{ section.settings.image | image_url: width: 2000 | image_tag: loading: 'eager', fetchpriority: 'high', sizes: '100vw', class: 'hero__image' }}
<div class="hero__content"><h1>{{ section.settings.title }}</h1></div>
</section>srcset dan sizes
Tanpa srcset dan sizes, browser cuma punya satu gambar buat semua viewport: mobile download gambar ukuran desktop, ngebuang bandwidth dan ngelambatin LCP. Cara kerjanya:
<img
src="image-800.jpg"
srcset="
image-400.jpg 400w,
image-600.jpg 600w,
image-800.jpg 800w,
image-1000.jpg 1000w
"
sizes="(min-width: 1000px) 900px, calc(100vw - 2rem)"
alt="Product"
/>Browser: cek lebar layout → evaluasi sizes buat tau ukuran tampil gambar → cek kepadatan piksel (2x Retina) → pilih gambar terkecil dari srcset yang cukup → download cuma itu.
Dengan image_tag:
{{ product.featured_image
| image_url: width: 1000
| image_tag:
widths: '400, 600, 800, 1000',
sizes: '(min-width: 1000px) 900px, calc(100vw - 2rem)'
}}Ngasilin srcset di lebar yang ditentuin plus width/height otomatis.
1. Kesalahan Umum
- Terlalu banyak lebar di
srcset: ngerusak efektivitas cache dan browser harus ngevaluasi lebih banyak opsi. 4–6 lebar biasanya cukup, gak perlu gambar sempurna buat tiap piksel. sizessalah: browser download gambar yang salah, bisa yang terbesar (buang bandwidth) atau terkecil (pecah).- Tanpa
sizes: browser default ke gambar terbesar, mobile buang bandwidth, LCP lambat.
2. Nulis sizes yang Bener
sizes itu terkenal susah. Pakai ekstensi Chrome Responsive Image Linter: install, isi sizes dengan nilai apapun, ekstensinya ngitung nilai yang bener, update kodemu. Jaga saran srcset-nya wajar.
3. sizes=“auto” buat Gambar Lazy
Buat gambar loading="lazy", set sizes ke auto daripada daftar breakpoint. Browser pakai lebar layout gambar itu sendiri buat milih source, jadi nilainya gak bisa melenceng dari CSS kamu. Shopify nyuntik polyfill buat browser yang belum ndukung, jadi aman dipakai sekarang:
{{ product.featured_image
| image_url: width: 1000
| image_tag:
loading: 'lazy',
widths: '400, 600, 800, 1000',
sizes: 'auto'
}}Peringatan: auto cuma buat loading="lazy". Gambar eager termasuk LCP tetap butuh sizes eksplisit, karena browser harus milih source sebelum layout. Dan jangan lazy-load gambar LCP demi auto: jeda lazy-load jauh lebih mahal dari sizes yang kurang sempurna.
4. Carousel, Slideshow, Grid
- Load cuma yang perlu: jangan render
<img>buat gambar yang gak kelihatan. Carousel render beberapa<img>pertama, JavaScript nambahin sisanya waktu pengguna navigasi. - Prioritaskan yang kelihatan: gambar pertama slideshow/grid eager,
fetchpriority="high"kalau LCP, sisanya lazy:
<div class="carousel">
{%- for product in collection.products limit: 6 -%}
{%- liquid
# The first slide is visible on load, so it stays eager and is the LCP candidate.
assign slide_loading = 'lazy'
assign slide_priority = 'auto'
if forloop.first
assign slide_loading = 'eager'
unless section.index > 1
assign slide_priority = 'high'
endunless
endif
-%}
<div class="carousel-slide">
{{ product.featured_image
| image_url: width: 800
| image_tag:
widths: '400, 600, 800',
sizes: '(min-width: 1000px) 33vw, 80vw',
loading: slide_loading,
fetchpriority: slide_priority
}}
</div>
{%- endfor -%}
</div>Cek dengan Performance > Insights (“Image sizes are too large”), Network panel di berbagai viewport, dan Device Mode.
width dan height: Nyegah CLS
Gambar tanpa width/height itu penyebab CLS paling umum di web. Tanpa dimensi: browser parse <img> → nganggap tingginya 0 → render tanpa ruang → download gambar → tau dimensinya → dorong konten buat ngasih ruang. Hasilnya CLS tinggi dan frustrasi.
Solusinya: selalu width dan height, dikombinasi CSS max-width: 100%; height: auto biar browser ngitung rasio dan mereservasi ruang sebelum download:
<img src="image.jpg" width="800" height="600" alt="Product" />img {
max-width: 100%;
height: auto;
}image_tag otomatis nambahin dimensi yang bener dari rasio gambar asli:
{{ product.featured_image
| image_url: width: 800
| image_tag: alt: product.title
}}Alternatif CSS modern: aspect-ratio: 16 / 9 di wadah, atau buat layout ala background: wadah dengan tinggi tetap dan object-fit: cover. Cek di Performance panel (entri Layout Shift dengan screenshot), Live metrics, dan Rendering > Layout Shift Regions.
Filter Chain, Bukan URL Manual
Server jauh lebih efisien ngebangun ukuran gambar daripada satu per satu. Waktu kamu bikin URL manual buat tiap lebar di srcset, tiap lebar itu panggilan filter terpisah yang nambah overhead Liquid. Satu image_tag ngebangun semua lebar srcset di server dalam satu operasi. Filter native dikompilasi ke bytecode, sedangkan manipulasi string itu Liquid interpreted yang jauh lebih lambat. Seberapa besar bedanya tergantung halaman: kecil di halaman dengan beberapa gambar, besar di halaman produk high-variant.
Yang hilang kalau manual: srcset otomatis, width/height yang bener, focal point, negosiasi format (WebP/AVIF otomatis dari CDN, hemat 25–35%), parameter CDN, versioning cache, Early Hints, dan rusak kalau Shopify ngubah struktur URL CDN.
Cari di theme: | replace: di konteks gambar, image.src langsung di <img>, srcset manual, width/height hardcode:
{%- comment -%} Anti-pattern: manual construction {%- endcomment -%}
<img src="{{ product.featured_image.src | replace: '.jpg', '_x800.jpg' }}" />
{%- comment -%} Recommended: filter chain {%- endcomment -%}
{{ product.featured_image | image_url: width: 800 | image_tag }}1. Cara srcset Dibangun
Lebar kandidat mulai dari default 352, 832, 1200, 1920. Kalau kamu kasih width ke image_url, lebar itu ditambahin dan di-sort ulang. Kalau kasih widths ke image_tag, daftar kamu ngegantiin default sepenuhnya. Terus set-nya disaring ke lebar yang gak lebih besar dari output image_url, jadi markup gak pernah minta gambar yang di-upscale. Kalau gak ada yang lolos, srcset gak di-emit sama sekali. Contoh: image_url: width: 800 + widths: '400, 600, 800' → srcset berisi 400w, 600w, 800w.
2. Early Hints
Parameter preload: true di image_tag memicu HTTP 103 Early Hints: instruksi preload dikirim sebelum HTML selesai dirender, jadi browser mulai download gambar lebih awal dari mekanisme discovery manapun. Pakai buat gambar LCP, dan gate optimasinya, bukan gambarnya, biar hero selalu render:
{% unless section.index > 1 %}
{{ section.settings.hero_image
| image_url: width: 2400
| image_tag:
widths: '800, 1200, 1600, 2000, 2400',
sizes: '100vw',
loading: 'eager',
fetchpriority: 'high',
preload: true
}}
{% else %}
{{ section.settings.hero_image
| image_url: width: 2400
| image_tag:
widths: '800, 1200, 1600, 2000, 2400',
sizes: '100vw',
loading: 'lazy'
}}
{% endunless %}Dialog yang Membajak LCP
JavaScript nyuntik kebanyakan dialog dan banner setelah HTML awal dimuat. Kalau dialog itu elemen terbesar di viewport waktu muncul, dia jadi elemen LCP dan ngereset jam LCP: browser ngelaporin waktu munculnya dialog, bukan waktu pengguna pertama lihat konten utama. Halaman yang ngerender hero di 1,5 detik bisa ngelaporin LCP 4 detik karena cookie banner muncul saat itu dan lebih besar dari hero.
1. Caranya
- Bikin dialog lebih kecil dari konten utama: banner bar sempit bukan overlay layar penuh, teks dialog lebih kecil dari heading utama, batasi tinggi dialog.
- Tunda dialog sampai interaksi pengguna: pengukuran LCP selesai di scroll pertama atau input pertama (klik, tap, keypress), jadi dialog yang muncul setelah itu gak bisa jadi LCP. Gate di interaksi, bukan timer: dialog dari
setTimeoutdi halaman yang belum disentuh tetap eligible, dan kalau lebih besar dari hero, jadi kandidat LCP yang lebih telat dan lebih buruk:
// Delay the popup until the user scrolls, clicks, taps, or types
function showPopup() {
var popup = document.getElementById('promo-popup');
if (popup) {
popup.removeAttribute('hidden');
}
}
var popupShown = false;
function triggerPopup() {
if (popupShown) return;
popupShown = true;
showPopup();
}
['scroll', 'pointerdown', 'keydown'].forEach(function (eventType) {
window.addEventListener(eventType, triggerPopup, { once: true, passive: true });
});- Kontrol lewat Liquid halaman mana yang punya dialog, biar markup dan style-nya gak dimuat di halaman yang gak butuh (misalnya cuma index dan koleksi, bukan produk dan cart yang ngeganggu alur beli):
{% if template.name == 'index' or template.name == 'collection' %}
<div id="email-popup" hidden class="popup-overlay">
<div class="popup-content">
<button class="popup-close" aria-label="Close">×</button>
<h2>{{ section.settings.popup_heading }}</h2>
<p>{{ section.settings.popup_text }}</p>
{% form 'customer' %}
<input type="email" name="contact[email]" placeholder="Your email" required>
<button type="submit">Subscribe</button>
{% endform %}
</div>
</div>
{% endif %}- Hapus kalau bisa: dialog email capture sebelum pengguna lihat konten itu anti-pola UX dengan konversi rendah. Alternatif: form inline di konten, banner non-modal atas/bawah, prompt setelah beberapa halaman.
Gak ada cara opt-out elemen dari LCP. Spesifikasinya gak pernah baca sinyal opt-out, dan elementtiming cuma buat Element Timing API, gak ngaruh ke skor LCP. Satu-satunya perbaikan: lebih kecil dari LCP asli, atau gak dirender sampai interaksi. Cek di Performance > Insights atau marker LCP di track Timings, dan tes mobile dan desktop: dialog yang lebih kecil dari hero di desktop bisa lebih besar di viewport mobile yang sempit.
picture buat Hero Mobile dan Desktop
Banyak theme ngerender dua <img> buat hero (desktop dan mobile) dan toggle dengan CSS display: none di breakpoint. Browser gak tau salah satunya disembunyiin, jadi download dua-duanya. Di mobile, hero desktop yang lebar dan resolusi tinggi ikut download, buang bandwidth dan bersaing koneksi. Masalah kedua: fetchpriority="high" sering cuma di gambar desktop karena gambar mobile dirender di jalur kode terpisah, jadi di mobile (60–75% trafik) gambar LCP download dengan prioritas default, nambah ratusan milidetik.
<picture> nyelesaiin dua-duanya: browser ngevaluasi <source media> dan download cuma yang cocok, dan fetchpriority="high" di <img> fallback berlaku ke source manapun yang dipilih:
{%- comment -%} After: single picture, browser downloads only the matching source {%- endcomment -%}
<picture>
{% if mobile_image != blank %}
<source
media="(max-width: 749px)"
srcset="{{ mobile_image | image_url: width: 600 }} 600w,
{{ mobile_image | image_url: width: 800 }} 800w,
{{ mobile_image | image_url: width: 1000 }} 1000w,
{{ mobile_image | image_url: width: 1200 }} 1200w"
sizes="100vw"
width="{{ mobile_image.width }}"
height="{{ mobile_image.height }}"
>
{% endif %}
<source
media="(min-width: 750px)"
srcset="{{ desktop_image | image_url: width: 1200 }} 1200w,
{{ desktop_image | image_url: width: 1600 }} 1600w,
{{ desktop_image | image_url: width: 2000 }} 2000w,
{{ desktop_image | image_url: width: 2560 }} 2560w"
sizes="100vw"
width="{{ desktop_image.width }}"
height="{{ desktop_image.height }}"
>
{{ desktop_image | image_url: width: 2560 | image_tag:
fetchpriority: fetch_priority,
loading: 'eager',
sizes: '100vw',
class: 'hero__image'
}}
</picture>Source mobile duluan biar browser milihnya waktu media query cocok, desktop sebagai fallback. width/height di tiap <source> biar rasio per source bener (hero mobile biasanya portrait). Ini satu-satunya tempat di bab ini srcset dibangun manual, karena <source> gak punya filter, tapi tetap pakai image_url per URL, bukan replace.
❌ dua gambar, dua-duanya download, fetchpriority cuma di desktop
{{ desktop_image | image_url: width: 2560 | image_tag: class: 'hero__image--desktop', fetchpriority: 'high' }}
{{ mobile_image | image_url: width: 1200 | image_tag: class: 'hero__image--mobile' }}✅ satu <picture>, satu download, prioritas ikut source yang dipilih
<picture>
<source media="(max-width: 749px)" srcset="...">
{{ desktop_image | image_url: width: 2560 | image_tag: fetchpriority: 'high', loading: 'eager' }}
</picture>Ringkasan
fetchpriority="high"cuma di satu gambar LCP per halaman, dengan eager, gabungkansection.index == 1(dannilke eager). Berlebihan malah lambat.- Jangan lazy-load gambar LCP: preload scanner nemuin
srcwaktu parse HTML,loading="lazy"dandata-srclibrary ngerusaknya. Buang library lazy-load, pakai defaultimage_tag(eager section 1–3 dannil),forloop.index <= 4buat grid,sizeseksplisit buat eager. - Jangan sembunyiin LCP di balik animasi:
opacity: 0ngecualiin dari LCP,display: nonelebih buruk (CLS juga). Matiin page transition di theme setting, kecualikan section pertama dengansection.index > 1, kalau harus pakai@keyframes< 0,3s. - Hero pakai
<img>absolut +object-fit: cover, bukan CSS background (ditemukan telat), tanpaz-index: -1. srcset/sizes: 4–6 lebar,sizesbener (Responsive Image Linter),sizes="auto"cuma buat lazy, gambar carousel/grid cuma yang kelihatan eager.width/heightselalu ada (otomatis dariimage_tag) +max-width: 100%; height: autobuat nyegah CLS.- Filter chain
image_url | image_tag, bukan URL manual:srcsetdari default 352/832/1200/1920 atauwidths, disaring ≤ lebarimage_url; WebP/AVIF, focal point, versioning, danpreload: truebuat Early Hints. - Dialog jangan jadi LCP: lebih kecil dari hero, atau tunda sampai scroll/pointerdown/keydown (bukan timer), batasi per template lewat Liquid, gak ada opt-out LCP.
- Hero mobile/desktop pakai
<picture>dengan source mobile duluan, bukan dua<img>yang di-toggle CSS.
Bab selanjutnya kita bahas CSS dan font: kurangi jumlah stylesheet, CLS dari font swap dan size-adjust, critical CSS sinkron, self-host font di CDN Shopify, system font, ruang buat konten app, animasi transform/opacity, mega menu tunggal, dan stylesheet subsetting.