Lazy Loading — Odlozeno ucitavanje resursa

Kako smanjiti inicijalni page load za 40-60% ucitavanjem resursa tek kad su potrebni

40-60%
Smanjenje inicijalnog load-a
96%
Browser podrska za loading=lazy
0
Linija JS-a potrebno (native)
LCP
NE lazy loadujte hero sliku!

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
Statistika: Lazy loading moze smanjiti inicijalni page load za 40-60% na stranicama sa mnogo slika (blog, e-commerce, galerije). Na nasem sajtu, 15 blog stranica u footeru se ne ucitavaju dok korisnik ne skroluje.

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
VrednostPonasanjeKada koristiti
loading="lazy"Ucitava tek kad je blizu viewport-aSlike/iframe ispod fold-a
loading="eager"Ucitava odmah (default)Hero slika, logo, kriticne slike
(bez atributa)Isto kao eager
Browser podrska: 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.

PozicijaloadingfetchpriorityPrimer
Hero slika (LCP)eager (ili bez atributa)highBanner, glavna slika
LogoeagerHeader logo
Above-fold slikeeagerPrvih 2-3 slike
Below-fold slikelazyClanak slike, galerija
Footer slikelazylowPartneri, badges
YouTube embedlazyVideo 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">
#1 greska: 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().

VrednostZnacenjeKada
highPrioritizuj ucitavanjeHero/LCP slika, kriticni CSS
lowSmanji prioritetManje vazne slike, prefetch
autoBrowser 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">
Kad koristiti Intersection Observer umesto native: Animacije pri pojavljivanju (fade-in, slide-up), beskonacni scroll (ucitavanje novih stavki), custom threshold, placeholder sa blur efektom.

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
});
Pravilo: Lazy loadujte sve sto korisnik ne vidi odmah: modalne dijaloge, tabove koji nisu aktivni, admin panele, teske biblioteke (charting, editore, mape). Inicijalni bundle treba biti <170KB kompresovan za 3s load na 4G.

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

Proverite lazy loading na vasem sajtu →

Lazy Loading — Deferred Resource Loading

How to reduce initial page load by 40-60% by loading resources only when needed

40-60%
Initial load reduction
96%
Browser support for loading=lazy
0
Lines of JS needed (native)
LCP
DON'T lazy load hero image!

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.

Statistic: Lazy loading can reduce initial page load by 40-60% on image-heavy pages.

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>
ValueBehaviorWhen
lazyLoad when near viewportBelow-fold images/iframes
eagerLoad immediately (default)Hero, logo, critical images

96%+ browser support. Old browsers load normally (graceful degradation).

3. Above vs below the fold

Positionloadingfetchpriority
Hero/LCP imageeagerhigh
Logoeager
Above-fold imageseager
Below-fold imageslazy
Footer imageslazylow
#1 mistake: 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().

Rule: Lazy load everything user doesn't see immediately: modals, inactive tabs, admin panels, heavy libraries. Initial bundle should be <170KB compressed.

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

Check lazy loading on your site →