Optimizacija JavaScript paketa u 2026: Code Splitting, Tree Shaking i moderne strategije za brži web

Praktični vodič za optimizaciju JavaScript paketa u 2026. — tree shaking, code splitting, Vite i Webpack konfiguracija, analiza paketa i utjecaj na Core Web Vitals.

Uvod: Zašto je veličina JavaScript paketa ključna za web performanse

JavaScript je kralježnica modernog weba — tu nema rasprave. Svaka interaktivna komponenta, svaki dinamički element, svaka animacija oslanja se na njega. Ali ta moć dolazi s cijenom. Prema podacima iz 2025. godine, prosječna web-stranica isporučuje preko 500 KB JavaScript koda, a kompleksnije aplikacije lako prelaze i megabajt.

I to nije samo suha statistika.

Razmislite o ovome: svaki kilobajt JavaScripta koji pošaljete pregledniku mora biti preuzet, parsiran, kompajliran i izvršen. Za razliku od slike koja se jednostavno prikaže na ekranu, JavaScript aktivno zauzima glavnu nit preglednika (main thread), blokirajući sve ostale operacije — uključujući korisničke interakcije. Korisnik klikne gumb, ali ništa se ne događa jer je preglednik zauzet izvršavanjem ogromnog paketa JavaScripta. Frustrirajuće, zar ne?

Upravo zato Interaction to Next Paint (INP) — Googleova metrika koja mjeri odzivnost stranice — postaje sve teža za zadovoljiti. Najnoviji podaci pokazuju da samo 47% web-stranica prolazi sve Core Web Vitals pragove, a INP je najčešći krivac. Teški JavaScript paketi izravno utječu na INP jer blokiraju main thread i sprečavaju brzi vizualni odgovor na korisničke akcije.

U ovom vodiču proći ćemo kroz sve ključne tehnike za optimizaciju JavaScript paketa — od tree shakinga i code splittinga, preko modernih alata poput Vitea i Webpacka, do analize paketa i upravljanja bibliotekama trećih strana. Cilj je jednostavan: da nakon čitanja imate konkretne, primjenjive strategije za ozbiljno smanjenje veličine vaših JavaScript paketa. Ajmo!

Razumijevanje problema: Anatomija JavaScript paketa

Prije nego što krenemo u optimizaciju, trebamo razumjeti što zapravo čini JavaScript paket i zašto raste nekontrolirano.

Što je JavaScript paket (bundle)?

JavaScript paket je jedna ili više datoteka koje nastaju procesom bundlanja — spajanja svih vaših modula, biblioteka i ovisnosti u datoteke spremne za isporuku pregledniku. Alati poput Webpacka, Vitea (koji koristi Rollup za produkciju) ili esbuild-a analiziraju vaš kod, prate sve import izjave i generiraju optimizirane izlazne datoteke.

Problem nastaje kada paket sadrži previše koda koji korisnik zapravo ne treba u tom trenutku. I iskreno, to se događa češće nego što biste pomislili. Tipični uzroci pretjeranih paketa uključuju:

  • Nekorišteni kod iz velikih biblioteka — importate cijeli lodash od 70 KB jer vam treba samo funkcija debounce
  • Sav kod učitan odjednom — korisnik posjećuje početnu stranicu, a preglednik preuzima JavaScript za sve podstranice
  • Teške biblioteke trećih strana — Moment.js sa svim lokalama (280 KB), kompletni icon paketi (150+ KB)
  • Duplikati ovisnosti — različite verzije iste biblioteke završe u paketu
  • Polyfilli koji više nisu potrebni — kod za podršku starih preglednika koje više nitko ne koristi

Tri veličine koje morate poznavati

Kada govorimo o veličini paketa, postoje tri ključne metrike:

  • Izvorna veličina (raw size) — veličina nekomprimiranog, neminificiranog koda
  • Minificirana veličina (minified) — nakon uklanjanja razmaka, komentara i skraćivanja varijabli, obično 30-50% manja
  • Komprimirana veličina (gzip/Brotli) — nakon kompresije na serveru, obično dodatnih 50-70% manje. Ovo je stvarna veličina koju korisnik preuzima

Uvijek mjerite gzip ili Brotli veličinu jer ona predstavlja stvarni mrežni trošak. Ali — i ovo je bitno — čak i komprimiran JavaScript mora biti dekomprimiran, parsiran i izvršen. Tako da minificirana veličina i dalje ostaje relevantna za procesorsko opterećenje uređaja.

Tree Shaking: Uklanjanje mrtvog koda

Tree shaking je tehnika koja automatski uklanja nekorišteni kod iz vašeg paketa. Naziv dolazi od metafore tresenja stabla — tresite stablo i mrtvo lišće otpada. U kontekstu JavaScripta, „mrtvo lišće" su funkcije, klase i varijable koje ste importali ali nikad ne koristite.

Zvuči jednostavno, ali u praksi može napraviti ogromnu razliku.

Kako tree shaking funkcionira

Tree shaking ovisi o statičkoj analizi ES modula (ESM). Za razliku od CommonJS-a (require()), ES moduli koriste import i export izjave koje su statički analizabilne — bundler može u fazi kompilacije točno odrediti koje eksporte koristite, a koje ne.

Evo jednostavnog primjera. Recimo da imate datoteku s pomoćnim funkcijama:

// utils.js
export function formatDate(date) {
  return date.toLocaleDateString('hr-HR');
}

export function formatCurrency(amount) {
  return new Intl.NumberFormat('hr-HR', {
    style: 'currency',
    currency: 'EUR'
  }).format(amount);
}

export function formatPercentage(value) {
  return `${(value * 100).toFixed(2)}%`;
}

export function deepClone(obj) {
  return structuredClone(obj);
}

Ako u svojoj aplikaciji koristite samo formatDate i formatCurrency:

// app.js
import { formatDate, formatCurrency } from './utils.js';

console.log(formatDate(new Date()));
console.log(formatCurrency(42.5));

Bundler s tree shakingom će automatski izbaciti formatPercentage i deepClone iz finalnog paketa jer ih nitko ne koristi. Manji paket, nula ručnog rada. Lijepo.

Zamke tree shakinga: sideEffects

Tree shaking nije magija — ima svoja ograničenja. Najveća zamka su nuspojave (side effects). Ako modul prilikom importa izvršava kod koji mijenja globalno stanje, bundler ga ne može sigurno ukloniti čak i ako ne koristite nijedan od njegovih eksporta.

Da biste pomogli bundleru, koristite polje sideEffects u package.json:

{
  "name": "moja-biblioteka",
  "sideEffects": false
}

Postavljanje sideEffects: false govori bundleru: „Svi moduli u ovom paketu su čisti — ako se ne koriste, slobodno ih izbaci." Ako neki moduli ipak imaju nuspojave (npr. CSS datoteke ili polyfilli), možete ih navesti eksplicitno:

{
  "sideEffects": [
    "*.css",
    "./src/polyfills.js"
  ]
}

Ovo je jedna od onih sitnica koje se lako zaborave, a mogu napraviti veliku razliku u konačnoj veličini paketa.

Tree shaking u praksi: precizni importi biblioteka

Jedan od najjednostavnijih načina za smanjenje paketa je prelazak na precizne importe umjesto importa cijelih biblioteka:

// LOŠ pristup — importa cijeli lodash (~70 KB)
import _ from 'lodash';
const result = _.debounce(myFunction, 300);

// DOBAR pristup — importa samo debounce (~2 KB)
import debounce from 'lodash/debounce';
const result = debounce(myFunction, 300);

// NAJBOLJI pristup — koristi modernu alternativu
// lodash-es podržava tree shaking nativno
import { debounce } from 'lodash-es';
const result = debounce(myFunction, 300);

Ova naizgled mala promjena može smanjiti doprinos lodash-a u vašem paketu s 70 KB na svega 2-5 KB. Pomnožite to s desetak biblioteka i ušteda postaje ogromna.

Code Splitting: Učitavanje koda po potrebi

Dok tree shaking uklanja kod koji se nikad ne koristi, code splitting razdvaja kod koji se koristi u različitim kontekstima u zasebne pakete (chunks) koji se učitavaju po potrebi. Umjesto jednog ogromnog paketa, korisnik preuzima samo ono što mu treba za trenutnu stranicu ili interakciju.

E sad, ovo je po mom mišljenju jedna od najmoćnijih optimizacija koje možete primijeniti.

Vrste code splittinga

Postoje tri glavne strategije:

1. Route-based splitting (po rutama) — najčešća i najučinkovitija strategija. Svaka ruta (stranica) dobiva vlastiti chunk:

// React primjer s React.lazy() i Suspense
import React, { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// Dinamički importi — svaka komponenta postaje zaseban chunk
const Home = lazy(() => import('./pages/Home'));
const ProductList = lazy(() => import('./pages/ProductList'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Checkout = lazy(() => import('./pages/Checkout'));
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Učitavanje...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/proizvodi" element={<ProductList />} />
          <Route path="/proizvodi/:id" element={<ProductDetail />} />
          <Route path="/naplata" element={<Checkout />} />
          <Route path="/admin" element={<AdminDashboard />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Ovim pristupom, korisnik koji posjećuje početnu stranicu ne preuzima JavaScript za admin panel ili stranicu za naplatu. Smanjenje inicijalnog paketa može biti 50-70% — a to nije mala stvar.

2. Component-based splitting (po komponentama) — za teške komponente koje se ne prikazuju odmah:

// Teški editor se učitava samo kad korisnik klikne "Uredi"
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));

function ArticlePage({ article }) {
  const [isEditing, setIsEditing] = useState(false);

  return (
    <div>
      <article>{article.content}</article>
      <button onClick={() => setIsEditing(true)}>Uredi</button>

      {isEditing && (
        <Suspense fallback={<p>Učitavanje editora...</p>}>
          <RichTextEditor content={article.content} />
        </Suspense>
      )}
    </div>
  );
}

3. Vendor splitting — odvajanje biblioteka trećih strana u zasebne chunkove koji se mogu agresivno keširati:

// vite.config.js — ručno razdvajanje vendor chunkova
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // React ekosustav u jednom chunku
          'vendor-react': ['react', 'react-dom', 'react-router-dom'],
          // UI biblioteka zasebno
          'vendor-ui': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
          // Utility biblioteke
          'vendor-utils': ['date-fns', 'zod']
        }
      }
    }
  }
});

Prefetching: Učitavanje unaprijed

Code splitting ima jednu potencijalnu manu — može povećati latenciju pri navigaciji jer se chunk mora preuzeti kad korisnik klikne na link. Rješenje? Prefetching — unaprijed učitavanje chunkova dok je korisnik još na trenutnoj stranici:

// Prefetch na hover — chunk se počne preuzimati kad korisnik prijeđe mišem
function NavLink({ to, children }) {
  const handleMouseEnter = () => {
    // Dinamički import s webpackChunkName za čitljivije nazive
    if (to === '/proizvodi') {
      import(/* webpackChunkName: "products" */ './pages/ProductList');
    }
  };

  return (
    <Link to={to} onMouseEnter={handleMouseEnter}>
      {children}
    </Link>
  );
}

Na ovaj način, kada korisnik konačno klikne link, chunk je već u preglednikovom kešu i navigacija se čini instantanom. Korisnici to ne primjećuju svjesno, ali definitivno osjete razliku.

Moderni alati: Vite vs Webpack u 2026. godini

Izbor pravog alata za bundlanje značajno utječe na kvalitetu optimizacije. U 2026. godini, dva dominantna alata su Vite i Webpack, svaki sa svojim prednostima.

Vite: Brzina i jednostavnost

Vite je rapidno preuzeo dominaciju u ekosustavu modernih frontend frameworka — i nije teško vidjeti zašto. Koristi esbuild (napisan u Go-u) za pre-bundlanje ovisnosti, što je 10 do 100 puta brže od JavaScript-baziranih alternativa. Za produkcijske buildove koristi Rollup, koji ima izvrsni tree shaking.

Ključne prednosti Vitea za optimizaciju paketa:

  • Nativna ESM podrška u developmentu — nema bundlanja tijekom razvoja, pa je dev server gotovo instantan
  • Rollup za produkciju — Rollupov tree shaking je dizajniran od temelja za ES module i često je efikasniji od Webpackovog
  • Automatski code splitting — dinamički importi automatski generiraju zasebne chunkove
  • CSS code splitting — CSS se automatski izvlači i razdvaja po chunkovima

Tipična Vite konfiguracija za optimizaciju izgleda ovako:

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    react(),
    // Vizualizacija paketa — generira HTML izvještaj
    visualizer({
      filename: 'bundle-report.html',
      gzipSize: true,
      brotliSize: true,
      open: true
    })
  ],
  build: {
    // Brotli kompresija za moderni preglednik
    target: 'es2020',
    // Upozorenje za chunkove veće od 500 KB
    chunkSizeWarningLimit: 500,
    rollupOptions: {
      output: {
        manualChunks(id) {
          // Automatsko odvajanje node_modules u vendor chunkove
          if (id.includes('node_modules')) {
            // Razdvoji velike biblioteke u zasebne chunkove
            if (id.includes('react') || id.includes('react-dom')) {
              return 'vendor-react';
            }
            if (id.includes('chart.js') || id.includes('d3')) {
              return 'vendor-charts';
            }
            return 'vendor';
          }
        }
      }
    },
    // Koristi Terser za bolju minifikaciju (sporije, ali manje datoteke)
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,  // Ukloni console.log u produkciji
        drop_debugger: true
      }
    }
  }
});

Webpack: Fleksibilnost i zrelost

Webpack ostaje relevantan za kompleksne projekte koji zahtijevaju finogranularnu kontrolu. Možda nije najnoviji alat na tržištu, ali njegove mogućnosti za optimizaciju paketa su iznimno bogate (iako zahtijevaju nešto više konfiguracije):

// webpack.config.js — produkcijska optimizacija
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const BundleAnalyzerPlugin =
  require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  mode: 'production',
  optimization: {
    // Omogući tree shaking
    usedExports: true,
    // Konfiguracija code splittinga
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: 25,
      minSize: 20000,
      cacheGroups: {
        // Razdvoji React u zaseban chunk
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'vendor-react',
          priority: 20,
          chunks: 'all'
        },
        // Razdvoji ostale vendor ovisnosti
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
          chunks: 'all',
          reuseExistingChunk: true
        },
        // Zajednički moduli iz aplikacije
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    },
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: { drop_console: true }
        }
      }),
      new CssMinimizerPlugin()
    ]
  },
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html'
    })
  ]
};

Usporedba: Vite vs Webpack za optimizaciju

Iz praktičnog iskustva, migracija s Webpack/CRA projekta na Vite često donosi 40-60% manje pakete zahvaljujući boljem tree shakingu Rollupa. Dev server se pokreće za manje od sekunde naspram 10+ sekundi s Webpackom — i ta razlika se osjeti svaki dan tijekom razvoja. No, Webpack i dalje nudi bolju podršku za starije preglednike i kompleksne enterprise scenarije, pa nije sve crno-bijelo.

Analiza paketa: Pronađite što vas usporava

Optimizacija bez mjerenja je nagađanje. Prije bilo kakvih promjena, trebate vizualizirati svoj paket i razumjeti točno koji moduli zauzimaju koliko prostora. Previše puta sam vidio developere koji intuitivno optimiziraju krive stvari — nemojte biti jedan od njih.

Webpack Bundle Analyzer

Webpack Bundle Analyzer generira interaktivnu mapu stabla (treemap) koja prikazuje relativnu veličinu svakog modula u paketu. Instalacija je jednostavna:

# Instalacija
npm install --save-dev webpack-bundle-analyzer

# Pokretanje analize
npx webpack --profile --json=stats.json
npx webpack-bundle-analyzer stats.json

Analyzer prikazuje tri veličine za svaki modul: stat (izvorna), parsed (minificirana) i gzip (komprimirana). Ova vizualizacija trenutno otkriva najveće probleme — možda ćete vidjeti da Moment.js zauzima 280 KB, ili da imate dvije verzije istog paketa. Iznenađenja su gotovo zagarantirana.

Rollup Plugin Visualizer (za Vite)

Za Vite projekte, rollup-plugin-visualizer nudi sličnu funkcionalnost:

# Instalacija
npm install --save-dev rollup-plugin-visualizer

Nakon konfiguracije u vite.config.js (kako smo pokazali ranije), pokrenite build i automatski će se generirati HTML izvještaj s interaktivnom vizualizacijom.

Česti nalazi i rješenja

Na temelju analize velikog broja projekata, evo najčešćih problema i kako ih riješiti:

  • Lodash (~70 KB) → Koristite lodash-es ili precizne importe (lodash/debounce). Još bolje — mnoge lodash funkcije danas imaju moderne native alternative.
  • Moment.js (~280 KB sa svim lokalama) → Zamijenite s date-fns (~12 KB s tree shakingom) ili nativnim Intl.DateTimeFormat.
  • Kompletni icon paketi (~150+ KB) → Importajte samo ikone koje koristite: import { FiSearch } from 'react-icons/fi' umjesto import * from 'react-icons'.
  • Nepotrebni polyfilli → Provjerite ciljane preglednike. U 2026. godini, podrška za ES2020+ je preko 95% — mnogi polyfilli su jednostavno nepotrebni.
  • Duplicirane ovisnosti → Koristite npm dedupe ili provjerite package-lock.json za višestruke verzije.

Utjecaj na Core Web Vitals: LCP, INP i CLS

Optimizacija JavaScript paketa nije samo tehnički poduhvat — ima izravan i mjerljiv utjecaj na Core Web Vitals, metrike koje Google koristi za rangiranje stranica. Dakle, ovo nije samo pitanje performansi, nego i SEO-a.

Largest Contentful Paint (LCP)

Prevelik JavaScript paket blokira renderiranje jer preglednik mora parsirati i izvršiti skripte prije prikaza sadržaja. Render-blocking JavaScript izravno povećava LCP. Strategije za poboljšanje:

  • Koristite atribute async ili defer na script tagovima
  • Prebacite nekritičan JavaScript u zasebne chunkove koji se učitavaju nakon inicijalnog renderiranja
  • Inline kritičan JavaScript potreban za iznad-pregiba (above-the-fold) sadržaj

Interaction to Next Paint (INP)

INP je metrika koja se u 2026. godini pokazala najtežom za zadovoljiti, posebno na mobilnim uređajima. Dobar INP rezultat je ispod 200 milisekundi. Zvuči puno, ali na sporijim mobilnim uređajima tih 200 ms prođe brže nego što mislite.

INP se sastoji od tri faze:

  1. Input delay — kašnjenje od korisničke akcije do pokretanja event handlera (često uzrokovano JavaScript zadacima koji blokiraju main thread)
  2. Processing time — vrijeme izvršavanja event handlera
  3. Presentation delay — vrijeme do vizualnog ažuriranja nakon obrade

Optimizacija paketa izravno smanjuje input delay jer manje JavaScripta znači manje dugotrajnih zadataka (long tasks) koji blokiraju main thread. Code splitting s dinamičkim importima može poboljšati INP za oko 100 ms na prosječnoj stranici — a to je razlika koju korisnici stvarno osjete.

Razbijanje dugih zadataka (Long Tasks)

Čak i nakon optimizacije paketa, neki JavaScript zadaci mogu trajati predugo i blokirati interakcije. Rješenje je razbijanje dugih zadataka u manje dijelove:

// LOŠ pristup — dugačak zadatak koji blokira main thread
function processLargeDataset(data) {
  const results = [];
  for (const item of data) {
    results.push(heavyComputation(item));
  }
  updateUI(results);
}

// DOBAR pristup — razbijanje na manje zadatke s yieldom
async function processLargeDataset(data) {
  const results = [];
  const CHUNK_SIZE = 100;

  for (let i = 0; i < data.length; i += CHUNK_SIZE) {
    const chunk = data.slice(i, i + CHUNK_SIZE);

    for (const item of chunk) {
      results.push(heavyComputation(item));
    }

    // Prepusti kontrolu pregledniku između chunkova
    await new Promise(resolve => setTimeout(resolve, 0));
  }

  updateUI(results);
}

// NAJBOLJI pristup u 2026. — koristi scheduler.yield() API
async function processLargeDataset(data) {
  const results = [];
  const CHUNK_SIZE = 100;

  for (let i = 0; i < data.length; i += CHUNK_SIZE) {
    const chunk = data.slice(i, i + CHUNK_SIZE);

    for (const item of chunk) {
      results.push(heavyComputation(item));
    }

    // scheduler.yield() je napredniji od setTimeout
    if ('scheduler' in globalThis && 'yield' in scheduler) {
      await scheduler.yield();
    } else {
      await new Promise(resolve => setTimeout(resolve, 0));
    }
  }

  updateUI(results);
}

Cumulative Layout Shift (CLS)

JavaScript paketi mogu neizravno uzrokovati pomake rasporeda (layout shifts) ako dinamički ubacuju sadržaj bez rezerviranog prostora. Pravilno korištenje code splittinga s Suspense komponentom koja prikazuje placeholder ispravne veličine pomaže u sprečavanju CLS problema. Ovo se često previdi, ali je važno.

Upravljanje bibliotekama trećih strana

Biblioteke trećih strana često čine 60-80% ukupnog JavaScript paketa. Da, dobro ste pročitali — većina koda koji isporučujete korisnicima nije vaš. Upravljanje njima je možda najvažniji aspekt optimizacije.

Strategija zamjene teških biblioteka

Evo konkretnog vodiča za zamjenu najčešćih „krivaca":

// PRIJE: Moment.js — ~280 KB (s lokalama)
import moment from 'moment';
const formatted = moment().format('DD.MM.YYYY');

// POSLIJE: date-fns — ~12 KB (s tree shakingom)
import { format } from 'date-fns';
import { hr } from 'date-fns/locale';
const formatted = format(new Date(), 'dd.MM.yyyy', { locale: hr });

// ILI: Nativni Intl API — 0 KB dodatnog JavaScripta!
const formatted = new Intl.DateTimeFormat('hr-HR').format(new Date());
// PRIJE: Axios — ~14 KB (gzip)
import axios from 'axios';
const { data } = await axios.get('/api/products');

// POSLIJE: Nativni fetch — 0 KB (ugrađen u preglednik)
const response = await fetch('/api/products');
const data = await response.json();
// PRIJE: classnames biblioteka — ~1 KB
import cn from 'classnames';
const cls = cn('btn', { 'btn-active': isActive });

// POSLIJE: Nativni template literal — 0 KB
const cls = `btn ${isActive ? 'btn-active' : ''}`.trim();

Naravno, nativne alternative ne pokrivaju uvijek sve edge caseove koje biblioteke pokrivaju. Ali za većinu uobičajenih slučajeva — sasvim su dovoljne.

Fasadni obrazac za teške widgete trećih strana

Analitike, chat widgeti, embedding mapa — sve to značajno povećava JavaScript teret. Fasadni obrazac (Facade Pattern) odgađa učitavanje ovih resursa dok korisnik ne pokaže namjeru interakcije:

// Fasadni obrazac za YouTube embed
function YouTubeFacade({ videoId, title }) {
  const [isLoaded, setIsLoaded] = useState(false);

  if (isLoaded) {
    return (
      <iframe
        src={`https://www.youtube.com/embed/${videoId}?autoplay=1`}
        title={title}
        allow="autoplay; encrypted-media"
        allowFullScreen
        style={{ width: '100%', aspectRatio: '16/9', border: 'none' }}
      />
    );
  }

  return (
    <button
      onClick={() => setIsLoaded(true)}
      style={{
        width: '100%',
        aspectRatio: '16/9',
        backgroundImage: `url(https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg)`,
        backgroundSize: 'cover',
        cursor: 'pointer',
        border: 'none',
        position: 'relative'
      }}
      aria-label={`Reproduciraj video: ${title}`}
    >
      <span style={{
        position: 'absolute',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%, -50%)',
        fontSize: '4rem'
      }}>▶</span>
    </button>
  );
}

Ovaj pristup eliminira preuzimanje stotina kilobajta YouTube iframe JavaScripta dok korisnik ne odluči pogledati video. Ista logika vrijedi za Google Maps, Intercom chat widgete ili bilo koji teški embed treće strane. Jednostavno, a učinkovito.

Kompresija i isporuka: Gzip, Brotli i CDN

Optimizacija samog koda je pola posla. Druga polovica je kako isporučujete taj kod pregledniku.

Brotli vs Gzip kompresija

Brotli (razvijen od strane Googlea) nudi 15-20% bolju kompresiju od gzipa za JavaScript datoteke. U 2026. godini, podrška za Brotli je preko 97% u preglednicima, pa iskreno — nema razloga ne koristiti ga.

Nginx konfiguracija za Brotli kompresiju:

# nginx.conf — Brotli i gzip kompresija
http {
    # Brotli kompresija (primarni)
    brotli on;
    brotli_comp_level 6;
    brotli_types text/plain text/css application/javascript
                 application/json image/svg+xml;

    # Gzip kao fallback
    gzip on;
    gzip_comp_level 6;
    gzip_types text/plain text/css application/javascript
               application/json image/svg+xml;

    # Dugotrajno keširanje za hashirane datoteke
    location ~* \.(js|css)$ {
        # Datoteke s hashom u imenu mogu se keširati zauvijek
        if ($uri ~* "\.[a-f0-9]{8,}\.") {
            expires max;
            add_header Cache-Control "public, immutable";
        }
    }
}

Precompressed assets

Umjesto da server komprimira datoteke pri svakom zahtjevu, možete ih prethodno komprimirati tijekom build procesa. Ovo je posebno korisno za statičke datoteke koje se rijetko mijenjaju:

// vite.config.js — prethodna kompresija
import { defineConfig } from 'vite';
import compression from 'vite-plugin-compression2';

export default defineConfig({
  plugins: [
    // Generiraj Brotli komprimirane datoteke
    compression({
      algorithm: 'brotliCompress',
      exclude: [/\.(br)$/, /\.(gz)$/]
    }),
    // Generiraj i gzip kao fallback
    compression({
      algorithm: 'gzip',
      exclude: [/\.(br)$/, /\.(gz)$/]
    })
  ]
});

Napredne strategije: Islands Architecture i parcijalna hidratacija

Tradicionalne Single Page Applications (SPA) šalju sav JavaScript klijentskoj strani za hidrataciju. Islands Architecture radikalno mijenja taj pristup — umjesto hidratacije cijele stranice, identificira samo interaktivne „otoke" i hidratira isključivo njih.

Ovo je, po mom mišljenju, jedan od najuzbudljivijih trendova u web developmentu zadnjih godina.

Frameworki poput Astro vode u ovom pristupu:

<!-- Astro komponenta — statički HTML bez JavaScripta -->
<header>
  <h1>Naslov stranice</h1>
  <p>Ovaj tekst je čisti HTML — nula JavaScripta.</p>
</header>

<!-- Interaktivni "otok" — hidratira se samo ova komponenta -->
<SearchWidget client:visible />

<!-- Još statičnog sadržaja -->
<main>
  <article>...sav sadržaj članka...</article>
</main>

<!-- Još jedan otok — učitava se samo kad postane vidljiv -->
<CommentSection client:visible />

Direktiva client:visible u Astru govori frameworku da hidratira komponentu tek kada ona postane vidljiva u viewportu korisnika. Rezultat? Stranice s minimalnim JavaScript teretom koje postižu izvrsne Core Web Vitals rezultate.

Prema mjerenjima iz 2025. godine, prebacivanje s tradicionalne SPA na Islands Architecture može smanjiti JavaScript isporuku za 80-90% na stranicama s pretežno statičkim sadržajem, poput blogova, dokumentacije ili e-commerce kataloga. To su impresivni brojevi.

Praktični vodič: Korak-po-korak optimizacija

Da sve ovo ne ostane samo teorija, evo konkretnog procesa koji možete primijeniti na svoj projekt već danas.

Korak 1: Mjerenje početnog stanja

# Instaliraj alat za analizu
npm install --save-dev rollup-plugin-visualizer  # za Vite
# ILI
npm install --save-dev webpack-bundle-analyzer    # za Webpack

# Pokreni produkcijski build s analizom
npm run build

# Provjeri veličinu ukupnog izlaza
du -sh dist/assets/*.js | sort -rh | head -20

Korak 2: Identifikacija najvećih modula

Otvorite generirani HTML izvještaj i identificirajte:

  • Top 5 najvećih modula iz node_modules
  • Duplicirane ovisnosti
  • Module koji se čine nepotrebnima

Često ćete se iznenaditi koliko „skrivenog" koda živi u vašem paketu.

Korak 3: Primjena optimizacija

  1. Zamijenite teške biblioteke lakšim alternativama ili nativnim API-jima
  2. Implementirajte route-based code splitting
  3. Konfigurirajte vendor chunking za bolje keširanje
  4. Provjerite sideEffects polje u package.json vaših modula
  5. Aktivirajte Brotli kompresiju na serveru

Korak 4: Mjerenje rezultata

# Usporedba veličina prije i poslije
echo "=== Prije optimizacije ==="
echo "Ukupni JS: 1.2 MB (gzip: 380 KB)"
echo ""
echo "=== Nakon optimizacije ==="
# Izmjerite stvarne veličine
find dist -name "*.js" -exec gzip -c {} \; | wc -c | \
  awk '{printf "Ukupni JS (gzip): %.2f KB\n", $1/1024}'

Pokrenite Lighthouse audit i usporedite Core Web Vitals prije i poslije. Očekivana poboljšanja:

  • LCP: 200-500 ms brže (manje render-blocking JavaScripta)
  • INP: 50-150 ms niže (manje long taskova na main threadu)
  • Total Blocking Time: 30-60% niže

Kontinuirano praćenje veličine paketa

Optimizacija paketa nije jednokratni poduhvat — i ovo je nešto što mnogi timovi nauče na teži način. Svaka nova ovisnost, svaki novi feature može ponovno napuhati paket. Zato je ključno uvesti kontinuirano praćenje u CI/CD pipeline:

// bundlesize.config.js — automatska provjera veličine u CI
module.exports = {
  files: [
    {
      path: 'dist/assets/index-*.js',
      maxSize: '150 kB',  // Glavni chunk
      compression: 'brotli'
    },
    {
      path: 'dist/assets/vendor-react-*.js',
      maxSize: '50 kB',   // React vendor chunk
      compression: 'brotli'
    },
    {
      path: 'dist/assets/*.js',
      maxSize: '300 kB',  // Ukupni JavaScript
      compression: 'brotli'
    }
  ]
};

S ovom konfiguracijom, svaki pull request koji prekorači definirane pragove automatski će biti označen, sprečavajući postupno narušavanje performansi koje se inače uoči tek kad je prekasno.

Zaključak: Manji paketi, brži web, bolji rezultati

Optimizacija JavaScript paketa u 2026. godini više nije opcija — to je nužnost za svaki ozbiljan web projekt. Kombinirajući tree shaking za uklanjanje nekorištenog koda, code splitting za učitavanje po potrebi, pametno upravljanje ovisnostima i modernu kompresiju, možete drastično smanjiti veličinu paketa i poboljšati korisničko iskustvo.

Ključne stvari za zapamtiti:

  • Mjerite prije optimizacije — koristite bundle analyzer da vidite stvarno stanje
  • ESM je temelj — bez ES modula nema učinkovitog tree shakinga
  • Code splitting po rutama je najjednostavnija optimizacija s najvećim učinkom
  • Zamijenite teške biblioteke nativnim API-jima kad god je moguće
  • Fasadni obrazac za teške embedove trećih strana štedi stotine kilobajta
  • Brotli kompresija dodatno smanjuje prenesenu veličinu za 15-20% naspram gzipa
  • Kontinuirano praćenje u CI/CD sprečava regresiju performansi

Web koji se brzo učitava nije samo tehnički cilj — to je temelj dobrog korisničkog iskustva, višeg SEO rangiranja i boljih poslovnih rezultata. Nadam se da vam je ovaj vodič dao konkretne alate i strategije za ozbiljnu optimizaciju JavaScript paketa na vašem projektu. Sretno s optimizacijom!

O Autoru Editorial Team

Our team of expert writers and editors.