Lazy Loading — Odlozeno ucitavanje resursa
Kako smanjiti inicijalni page load za 40-60% ucitavanjem resursa tek kad su potrebni
Sadrzaj
1. Sta je lazy loading i zasto je bitan
Lazy loading je tehnika koja odlaze ucitavanje resursa dok nisu vidljivi na ekranu. Umesto da browser ucita svih 50 slika na stranici odjednom, ucitava samo onih 3-4 vidljivih, a ostale tek kad korisnik skroluje do njih.
- Brzi inicijalni load: Manje podataka na pocetku = stranica se prikazuje brze
- Usteda bandwidth-a: Korisnik koji pogleda samo vrh stranice ne ucitava slike na dnu
- Bolji LCP: Browser prioritizuje kriticne resurse umesto da ucitava sve paralelno
- Manji Data Transfer: Mobilni korisnici na ogranicenom planu trse manje podataka
2. Native lazy loading: loading="lazy"
Najjednostavniji nacin — jedan HTML atribut, nula JavaScript-a:
<!-- Lazy loading za slike -->
<img src="photo.webp" alt="Opis" width="800" height="450" loading="lazy">
<!-- Lazy loading za iframe -->
<iframe src="https://youtube.com/embed/abc123" loading="lazy"
width="560" height="315"></iframe>
<!-- Eager loading (default) — za kriticne slike -->
<img src="hero.webp" alt="Hero" width="1200" height="630" loading="eager">
Kako browser odlucuje kad da ucita
Browser koristi heuristiku baziranu na:
- Udaljenost od viewport-a: Slika se pocinje ucitavati kad je ~1250px od vidljivog dela (zavisi od brzine konekcije)
- Brzina konekcije: Na sporijim konekcijama, browser pocinje ucitavanje ranije (veci threshold)
- Data Saver: Ako je ukljucen, threshold je jos veci
| Vrednost | Ponasanje | Kada koristiti |
|---|---|---|
loading="lazy" | Ucitava tek kad je blizu viewport-a | Slike/iframe ispod fold-a |
loading="eager" | Ucitava odmah (default) | Hero slika, logo, kriticne slike |
| (bez atributa) | Isto kao eager | — |
loading="lazy" podrzava 96%+ modernih browsera (Chrome 77+, Firefox 75+, Safari 15.4+, Edge 79+). Za preostalih ~4%, slika se ucitava normalno (graceful degradation).3. Above-the-fold vs Below-the-fold pravilo
Above-the-fold = deo stranice vidljiv bez skrolovanja. Below-the-fold = sve ispod toga.
| Pozicija | loading | fetchpriority | Primer |
|---|---|---|---|
| Hero slika (LCP) | eager (ili bez atributa) | high | Banner, glavna slika |
| Logo | eager | — | Header logo |
| Above-fold slike | eager | — | Prvih 2-3 slike |
| Below-fold slike | lazy | — | Clanak slike, galerija |
| Footer slike | lazy | low | Partneri, badges |
| YouTube embed | lazy | — | Video u clanku |
<!-- Hero — NIKAD lazy, visok prioritet -->
<img src="hero.webp" alt="Hero" width="1200" height="630"
fetchpriority="high" decoding="async">
<!-- Clanak slike — lazy -->
<img src="diagram.webp" alt="Dijagram" width="800" height="450" loading="lazy">
<img src="screenshot.webp" alt="Screenshot" width="800" height="450" loading="lazy">
<img src="chart.webp" alt="Grafikon" width="800" height="450" loading="lazy">
loading="lazy" na hero slici usporava LCP jer browser ceka da korisnik skroluje blizu pre ucitavanja. Hero slika mora biti eager + fetchpriority="high".4. fetchpriority atribut
fetchpriority govori browseru koliko je vazan resurs u odnosu na ostale. Radi sa <img>, <link>, <script> i fetch().
| Vrednost | Znacenje | Kada |
|---|---|---|
high | Prioritizuj ucitavanje | Hero/LCP slika, kriticni CSS |
low | Smanji prioritet | Manje vazne slike, prefetch |
auto | Browser odlucuje (default) | Sve ostalo |
<!-- Kombinacija: preload + fetchpriority za najbrzi LCP -->
<link rel="preload" as="image" href="hero.webp" fetchpriority="high">
<!-- Hero slika -->
<img src="hero.webp" alt="Hero" width="1200" height="630"
fetchpriority="high" decoding="async">
<!-- Manje vazna slika — smanji prioritet -->
<img src="decoration.webp" alt="Dekoracija" width="200" height="200"
fetchpriority="low" loading="lazy">
5. Intersection Observer API
Za custom lazy loading gde native loading="lazy" nije dovoljan (animacije pri skrolovanju, beskonacni scroll, custom threshold):
// Custom lazy loading sa Intersection Observer
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // Ucitaj pravu sliku
img.classList.add('loaded'); // Dodaj fade-in animaciju
observer.unobserve(img); // Prestani da posmatras
}
});
}, {
rootMargin: '200px', // Pocni 200px pre viewport-a
threshold: 0.01 // Triggeruj kad je 1% vidljiv
});
// Observiraj sve slike sa data-src atributom
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
/* HTML: */
// <img data-src="photo.webp" alt="Opis" width="800" height="450"
// src="placeholder.svg" class="lazy">
6. Lazy loading JavaScript: dynamic import()
Ne ucitavajte sav JavaScript odjednom. import() ucitava module tek kad su potrebni:
// UMESTO: import Chart from 'chart.js'; (ucitava odmah, 200KB)
// KORISTITE: ucitaj tek kad korisnik klikne na tab sa grafikonima
document.getElementById('charts-tab').addEventListener('click', async () => {
const { Chart } = await import('chart.js'); // Ucitava tek sada
const chart = new Chart(canvas, config);
});
// Ucitaj tezak modul tek kad je potreban
async function openEditor() {
const { Editor } = await import('./heavy-editor.js'); // 500KB
return new Editor('#container');
}
// Prefetch u idle vreme (ucitaj unapred ali ne izvrsi)
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
import('./analytics.js'); // Pripremi za kasniju upotrebu
});
}
7. Code splitting u React/Next.js/Vue
React (React.lazy + Suspense)
import React, { lazy, Suspense } from 'react';
// Ucitaj komponentu tek kad je potrebna
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));
function App() {
return (
<Suspense fallback={<div>Ucitavanje...</div>}>
{showChart && <HeavyChart />}
{isAdmin && <AdminPanel />}
</Suspense>
);
}
Next.js (next/dynamic)
import dynamic from 'next/dynamic';
// SSR: false — ucitaj samo na klijentu
const Map = dynamic(() => import('./Map'), {
ssr: false,
loading: () => <p>Ucitavanje mape...</p>
});
// Automatski code-split za svaku stranicu (pages/ direktorijum)
Vue (defineAsyncComponent)
import { defineAsyncComponent } from 'vue';
const AsyncChart = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
);
// Sa loading i error komponentama
const AsyncEditor = defineAsyncComponent({
loader: () => import('./Editor.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200, // Prikazi loading posle 200ms
timeout: 10000 // Timeout posle 10s
});
8. Lazy loading iframe-ova
<!-- YouTube embed — lazy -->
<iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ"
width="560" height="315" loading="lazy"
title="Video naslov"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
allowfullscreen></iframe>
<!-- Google Maps — lazy -->
<iframe src="https://www.google.com/maps/embed?pb=..."
width="600" height="450" loading="lazy"
style="border:0" allowfullscreen
referrerpolicy="no-referrer-when-downgrade"></iframe>
YouTube embed ucitava ~800KB resursa. Sa loading="lazy", to se ucitava tek kad korisnik skroluje do videa. Za jos bolju optimizaciju, koristite lite-youtube-embed — prikazuje thumbnail, ucitava iframe tek na klik (usteda: ~800KB → ~5KB).
9. Najcesce greske
- Lazy loading hero/LCP slike — #1 greska. Usporava LCP. Hero mora biti eager + fetchpriority="high".
- Lazy loading above-the-fold slika — Korisnik vidi prazno mesto dok se slika ucitava. Lazy samo za below-fold.
- Bez width/height na lazy slikama — Browser ne zna koliko prostora da rezervise = CLS kad se slika ucita.
- Previse aggressive threshold — Intersection Observer sa rootMargin: 0 ucitava sliku tek kad je vidljiva = korisnik vidi placeholder.
- Lazy loading svih slika — Prvih 2-3 slike (above-fold) trebaju biti eager. Samo below-fold je lazy.
- Lazy loading CSS/fontova — Kriticni CSS i fontovi moraju se ucitati odmah. Lazy samo za nekriticne resurse.
- JavaScript lazy loading bez code splitting — dynamic import() bez Webpack/Vite ne deli bundle. Koristite build tool.
- Nedostaje fallback za stare browsere — Native
loading="lazy"ima graceful degradation (ucitava normalno). Za Intersection Observer, dodajte polyfill ili fallback.
10. Reference i resursi
- web.dev — Browser-Level Lazy Loading
- web.dev — Lazy Loading Images and Video
- MDN — loading attribute
- MDN — Intersection Observer API
- web.dev — Fetch Priority
- MDN — Dynamic import()
- lite-youtube-embed
- Can I Use — loading=lazy
Lazy Loading — Deferred Resource Loading
How to reduce initial page load by 40-60% by loading resources only when needed
Table of Contents
1. What is lazy loading and why it matters
Lazy loading defers loading resources until they're visible on screen. Instead of loading all 50 images at once, browser loads only the 3-4 visible ones, and the rest when user scrolls to them.
2. Native lazy loading: loading="lazy"
<img src="photo.webp" alt="Desc" width="800" height="450" loading="lazy">
<iframe src="https://youtube.com/embed/..." loading="lazy"></iframe>
| Value | Behavior | When |
|---|---|---|
lazy | Load when near viewport | Below-fold images/iframes |
eager | Load immediately (default) | Hero, logo, critical images |
96%+ browser support. Old browsers load normally (graceful degradation).
3. Above vs below the fold
| Position | loading | fetchpriority |
|---|---|---|
| Hero/LCP image | eager | high |
| Logo | eager | — |
| Above-fold images | eager | — |
| Below-fold images | lazy | — |
| Footer images | lazy | low |
loading="lazy" on hero image slows LCP. Hero must be eager + fetchpriority="high".4. fetchpriority attribute
fetchpriority="high" on hero image, low on decorative images, auto (default) for everything else.
<link rel="preload" as="image" href="hero.webp" fetchpriority="high">
<img src="hero.webp" fetchpriority="high" decoding="async">
5. Intersection Observer API
For custom lazy loading: animations on scroll, infinite scroll, custom thresholds, blur-up placeholders.
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
}
});
}, { rootMargin: '200px' });
6. JS lazy loading: dynamic import()
// Load heavy module only when needed
button.addEventListener('click', async () => {
const { Chart } = await import('chart.js');
new Chart(canvas, config);
});
7. Code splitting in frameworks
React: React.lazy() + Suspense. Next.js: next/dynamic with ssr: false. Vue: defineAsyncComponent().
8. Lazy loading iframes
YouTube embed loads ~800KB. With loading="lazy", only loads when user scrolls to video. For better: use lite-youtube-embed (~800KB → ~5KB until click).
9. Common mistakes
- Lazy loading hero image — slows LCP
- Lazy above-fold images — user sees empty space
- No width/height on lazy images — causes CLS
- Too aggressive threshold — rootMargin: 0 shows placeholder
- Lazy loading ALL images — first 2-3 should be eager
- Lazy loading CSS/fonts — critical CSS must load immediately
10. References and resources
- web.dev — Lazy Loading
- MDN — loading attribute
- MDN — Intersection Observer
- web.dev — Fetch Priority
- MDN — Dynamic import()
- Can I Use — loading=lazy