Miért a JavaScript bundle méret a webes teljesítmény Achilles-sarka?
Ha fejlesztőként dolgozol, valószínűleg te is érezted már: az alkalmazásod egyre lassabb, a bundle egyre nagyobb, a felhasználók pedig egyre türelmetlenebbek. Nem vagy ezzel egyedül. A HTTP Archive 2025-ös Web Almanac adatai szerint a medián weboldal 23 JavaScript kérést indít asztali gépen és 22-t mobilon, miközben az oldalméretek mediánja elérte a 2,9 MB-ot desktopra — ami 7,3%-os növekedés 2024-hez képest. Ami igazán elgondolkodtató: a medián weboldal 280 KB kihasználatlan JavaScript kódot szállít le asztali gépen, mobilon pedig 251 KB-ot. A 90. percentilisnél ez meghaladja az 1 MB-ot.
Ezek a számok egyértelműek: a JavaScript bundle optimalizálás 2026-ban nem opcionális luxus, hanem a webes teljesítmény egyik legkritikusabb területe. Különösen most, amikor a Google Core Web Vitals mutatói — köztük az INP (Interaction to Next Paint) — közvetlenül befolyásolják a keresési rangsorolást.
Na de elég a riogatásból. Ebben az útmutatóban végigmegyünk a JavaScript bundle optimalizálás minden fontos aspektusán: a code splitting alapjaitól a tree shaking finomságain át a modern bundler-ek lehetőségeiig. Gyakorlati kódpéldákat és mérhető eredményeket mutatok be, amelyeket azonnal alkalmazhatsz a projektjeidben.
A JavaScript bundle probléma anatómiája
Mielőtt az optimalizálási technikákba merülünk, érdemes megérteni, miért is olyan problémás egy nagy JavaScript bundle. A böngészőnek három fő lépésen kell végigmennie minden letöltött JavaScript fájllal:
- Letöltés: A fájl letöltése a hálózaton keresztül. Lassú mobilhálózaton egy 500 KB-os tömörített bundle is több másodpercet igényelhet.
- Elemzés (parsing): A böngészőnek le kell elemezni a JavaScript forráskódot és AST-vé (Abstract Syntax Tree) kell alakítania. Ez CPU-intenzív művelet, ami különösen régebbi mobilokon fáj.
- Végrehajtás (execution): A kód futtatása a fő szálon (main thread), ami közben blokkolja a felhasználói interakciókat.
A fő szál blokkolása közvetlen hatással van az INP mutatóra. Ha a felhasználó kattint, gördít vagy begépel valamit, miközben a böngésző egy nagy JavaScript bundle-t dolgoz fel, a válaszidő drámaian megnő. A Google elvárása, hogy az INP 200 ms alatt maradjon — és őszintén szólva, a felesleges JavaScript feldolgozás az egyik leggyakoribb ok, amiért az oldalak nem teljesítik ezt.
A JavaScript költségének mérése
Sokan csak a fájlméretet nézik, de a bundle méret önmagában nem árulkodik a valódi költségről. Egy 200 KB-os JavaScript fájl feldolgozása akár ötször annyi időt vehet igénybe, mint egy 200 KB-os kép dekódolása — mert a JavaScript-et nem csak le kell tölteni, hanem elemezni, fordítani és végrehajtani is kell. Ezt a Chrome DevTools Performance fülén, a Lighthouse Reduce unused JavaScript javaslatával, vagy a Coverage eszközzel (Ctrl+Shift+P → Show Coverage) tudod mérni.
// A bundle méret ellenőrzése build után - webpack-bundle-analyzer
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
openAnalyzer: false,
}),
],
};
// Vite esetében: rollup-plugin-visualizer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({
filename: 'bundle-stats.html',
gzipSize: true,
brotliSize: true,
}),
],
});
Code Splitting: csak azt töltsd be, amire tényleg szükség van
A code splitting (kódszétválasztás) talán a leghatékonyabb fegyver a JavaScript bundle méretének csökkentésére. Az alapelv szinte banálisan egyszerű: ahelyett, hogy egyetlen hatalmas fájlban szállítjuk le az alkalmazás teljes kódját, kisebb darabokra — úgynevezett chunk-okra — bontjuk, és csak azokat töltjük be, amelyekre az adott oldalon szükség van.
Útvonal-alapú code splitting (Route-based)
A legegyszerűbb és talán leghatékonyabb belépési pont az útvonal-alapú code splitting. SPA alkalmazásoknál minden útvonal saját chunk-ot kap, így csak az aktuálisan meglátogatott oldal kódja töltődik le.
// React - Route-based code splitting React.lazy()-vel
import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Statikus import helyett dinamikus import
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div className="loading-spinner" />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Vue.js-ben ez még egyszerűbb (a Vue Router szinte kínálja magát erre):
// Vue Router - automatikus code splitting
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
component: () => import('./views/Home.vue'),
},
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'),
},
{
path: '/analytics',
// Named chunk a jobb debuggolásért
component: () => import(/* webpackChunkName: "analytics" */ './views/Analytics.vue'),
},
];
Komponens-szintű code splitting
Az útvonal-alapú szétválasztáson túl érdemes a nehéz komponenseket is külön chunk-ba szervezni. Tipikus jelöltek: modális ablakok, diagramok, szöveges editorok, dátumválasztók — lényegében minden, ami nem jelenik meg azonnal az oldal betöltésekor.
// Nehéz komponens lazy loading-ja
import React, { Suspense, lazy, useState } from 'react';
// Ez a chunk csak akkor töltődik le, amikor a felhasználó megnyitja a szerkesztőt
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
const ChartDashboard = lazy(() => import('./components/ChartDashboard'));
function ArticlePage({ article }) {
const [isEditing, setIsEditing] = useState(false);
const [showCharts, setShowCharts] = useState(false);
return (
<div>
<article>{article.content}</article>
<button onClick={() => setIsEditing(true)}>Szerkesztés</button>
{isEditing && (
<Suspense fallback={<p>Szerkesztő betöltése...</p>}>
<RichTextEditor content={article.content} />
</Suspense>
)}
<button onClick={() => setShowCharts(true)}>Statisztikák</button>
{showCharts && (
<Suspense fallback={<p>Diagramok betöltése...</p>}>
<ChartDashboard data={article.stats} />
</Suspense>
)}
</div>
);
}
Vendor splitting: a harmadik féltől származó kód elkülönítése
A harmadik féltől származó könyvtárak (lodash, moment, chart.js stb.) ritkán változnak, amikor frissíted az alkalmazáskódot. Ha ezeket külön chunk-ba szervezed, a böngésző cache-ből szolgálhatja ki őket egy-egy deploy után is. Ez drasztikusan csökkenti a visszatérő felhasználók betöltési idejét — és ők alkotják a forgalmad nagy részét.
// webpack.config.js - Optimalizált vendor splitting
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// Fő framework chunk (ritkán változik)
framework: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router)[\\/]/,
name: 'framework',
priority: 40,
enforce: true,
},
// Nagyobb könyvtárak külön chunk-ban
largeVendors: {
test: /[\\/]node_modules[\\/](chart\.js|lodash|moment|date-fns)[\\/]/,
name: 'large-vendors',
priority: 30,
enforce: true,
},
// Minden egyéb vendor
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 20,
reuseExistingChunk: true,
},
// Közös alkalmazáskód
common: {
minChunks: 2,
priority: 10,
reuseExistingChunk: true,
},
},
},
},
};
Tree Shaking: szabadulj meg a felesleges kódtól
A tree shaking (szó szerint „fa rázás") a bundler azon képessége, hogy statikus elemzéssel felismeri és eltávolítja a nem használt kódot a végső bundle-ből. A metafora onnan ered, hogy ha megrázod a fát, a száraz ágak — vagyis a nem használt kódrészletek — lehullanak.
Egyszerűen hangzik, igaz? A gyakorlatban viszont van néhány buktatója.
A tree shaking működésének feltétele: ES modulok
A tree shaking kizárólag ES6 modul szintaxissal (import / export) működik, mert ez statikusan elemezhető. A CommonJS (require / module.exports) dinamikus természete miatt nem alkalmas rá — a bundler egyszerűen nem tudja kitalálni, hogy futásidőben mire lesz szükség.
// ❌ ROSSZ - CommonJS: nem tree-shakeable
const { debounce } = require('lodash');
// Ez az EGÉSZ lodash könyvtárat beemeli (~70 KB gzip)
// ✅ JÓ - ES modul: tree-shakeable
import { debounce } from 'lodash-es';
// Csak a debounce függvény és függőségei kerülnek a bundle-be
// ✅ MÉG JOBB - Közvetlen import
import debounce from 'lodash/debounce';
// Garantáltan csak a debounce kerül be
A sideEffects mező helyes konfigurálása
A package.json sideEffects mezője kritikus a hatékony tree shaking-hez. Ez mondja meg a bundler-nek, hogy a modul melyik fájljai rendelkeznek mellékhatásokkal, és melyeket lehet biztonságosan törölni, ha senki sem importálja őket.
// package.json
{
"name": "my-library",
"version": "1.0.0",
"sideEffects": false,
// VAGY specifikusan megjelölve a mellékhatással bíró fájlokat:
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js",
"./src/analytics-init.js"
]
}
Ha a sideEffects értéke false, a bundler bátran eltávolíthat minden nem importált exportot. Ha viszont nincs beállítva? Akkor a bundler kénytelen konzervatívan viselkedni és mindent megtartani — ami feleslegesen duzzasztja a bundle-t.
Tree shaking problémák diagnosztizálása
A tree shaking sajnos nem mindig működik tökéletesen. Lássuk a leggyakoribb csapdákat:
// 1. probléma: Barrel exports akadályozzák a tree shaking-et
// ❌ src/components/index.js (barrel file)
export { default as Button } from './Button';
export { default as Modal } from './Modal'; // ~50 KB
export { default as DataGrid } from './DataGrid'; // ~120 KB
export { default as Chart } from './Chart'; // ~80 KB
// Ha csak a Button-t importálod:
import { Button } from './components';
// Egyes bundler konfigurációknál az ÖSSZES komponens bekerülhet!
// ✅ Megoldás: közvetlen import
import Button from './components/Button';
// 2. probléma: Dinamikus property hozzáférés
// ❌ Nem tree-shakeable
const icons = { home: HomeIcon, user: UserIcon, settings: SettingsIcon };
const Icon = icons[iconName]; // A bundler nem tudja, melyikre lesz szükség
// ✅ Megoldás: explicit import
import HomeIcon from './icons/Home';
// 3. probléma: Globális mellékhatások
// ❌ Ez a fájl nem törölhető, mert mellékhatása van
import './polyfills'; // Globális objektumot módosít
Array.prototype.customMethod = function() { /* ... */ };
Modern bundler-ek összehasonlítása 2026-ban
A JavaScript bundler ökoszisztéma 2026-ra alaposan átalakult. A Rust és Go nyelven írt, natív sebességű eszközök átvették a vezetést a korábbi JavaScript-alapú megoldásoktól. Nézzük a főbb szereplőket.
Vite 8 és Rolldown: az új alapértelmezett
A Vite 8 béta verziója hozta el a Rolldown integrációt — egy Rust-alapú bundler-t, amely a Rollup drop-in helyettesítőjeként működik, de 10-30x gyorsabb annál. Korábban a Vite fejlesztési módban esbuild-et, production build-hez pedig Rollup-ot használt. A Rolldown mindkettőt egységesíti egyetlen, natív sebességű eszközben. (Személyes véleményem, hogy ez volt az egyik legjobb lépés a Vite ökoszisztémában.)
// vite.config.js - Vite 8 optimalizált konfiguráció
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
// Rolldown automatikusan aktív Vite 8-ban
target: 'es2022',
// Chunk méret figyelmeztetés küszöbérték
chunkSizeWarningLimit: 250, // KB
rollupOptions: {
output: {
// Manuális chunk szétválasztás
manualChunks: (id) => {
if (id.includes('node_modules')) {
// React ökoszisztéma külön chunk
if (id.includes('react') || id.includes('react-dom')) {
return 'react-vendor';
}
// Egyéb vendor-ok
return 'vendor';
}
},
},
},
},
});
Rspack: Webpack-kompatibilis Rust-alapú bundler
Ha meglévő Webpack projekttel dolgozol és nem akarod az egészet újraírni, az Rspack kiváló átmeneti megoldás. A legtöbb Webpack konfigurációval kompatibilis, miközben a build idők drámaian csökkennek — jellemzően 5-10x gyorsulás érhető el, ami nagyobb projekteken naponta akár órákat spórolhat a csapatnak.
Teljesítmény összehasonlítás (benchmark adatok)
Az rspack csapat által karbantartott build-tools-performance benchmark alapján a kép elég egyértelmű:
- esbuild és Rolldown: 1 másodperc alatti build idő — az esbuild akár 250 ms alatt végez
- Rspack: ~2,5 másodperc
- Vite (Rollup-pal), Rollup, Webpack: 5+ másodperc
- Bundle méret: Rollup, Vite, esbuild, Rolldown és Parcel hasonlóan kis bundle-t produkálnak (~2081–2162 KiB a benchmark projektben)
A tanulság: a build sebesség önmagában nem minden. A bundle méret és a tree shaking hatékonysága legalább annyira fontos. A Vite 8 (Rolldown-nal) itt kombinált előnyt kínál — natív sebesség és kiváló tree shaking egyben.
A fő szál tehermentesítése: INP optimalizálás JavaScript szinten
A bundle méret csökkentése csak a történet fele. A másik — és legalább ilyen fontos — fele az, hogyan futtatjuk a JavaScript kódot. Pontosabban: hogyan kerüljük el, hogy a fő szál (main thread) hosszú időre beragadjon, ami közvetlenül rontja az INP-t.
Hosszú feladatok szétbontása (Long Task breaking)
A böngésző fő szála egyszerre vagy JavaScript-et futtat, vagy a felhasználói interakciókra reagál — a kettő egyszerre nem megy. Ha egy JavaScript feladat 50 ms-nél tovább tart (ezt hívjuk „Long Task"-nak), a felhasználó érezhetően lassúnak fogja tapasztalni az oldalt.
A megoldás? Bontsuk kisebb darabokra a hosszú feladatokat, és adjunk lehetőséget a böngészőnek, hogy közben feldolgozza a felhasználói eseményeket.
// ❌ ROSSZ - Egy hosszú, blokkoló feladat
function processLargeDataset(items) {
for (const item of items) {
// Nehéz számítás minden elemre
complexCalculation(item);
updateDOM(item);
}
}
// ✅ JÓ - scheduler.yield() használata (modern böngészők)
async function processLargeDatasetOptimized(items) {
const BATCH_SIZE = 50;
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const batch = items.slice(i, i + BATCH_SIZE);
for (const item of batch) {
complexCalculation(item);
updateDOM(item);
}
// Visszaadjuk a vezérlést a böngészőnek
// A scheduler.yield() a feladat elején folytatja, nem a sor végén
if (typeof scheduler !== 'undefined' && scheduler.yield) {
await scheduler.yield();
} else {
// Fallback régebbi böngészőkhöz
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
A Scheduler API és feladatprioritizálás
A modern böngészők Scheduler API-ja három prioritási szintet kínál, és érdemes kihasználni mindet:
// Scheduler API - feladatok priorizálása
// 1. user-blocking: Kritikus interakciók (pl. input kezelés)
scheduler.postTask(() => {
handleUserClick();
}, { priority: 'user-blocking' });
// 2. user-visible: Fontos, de nem azonnali (pl. lista frissítés)
scheduler.postTask(() => {
updateProductList(newData);
}, { priority: 'user-visible' });
// 3. background: Alacsony prioritás (pl. analitika, prefetch)
scheduler.postTask(() => {
sendAnalyticsEvent('page_view', pageData);
prefetchNextPage();
}, { priority: 'background' });
// requestIdleCallback - a böngésző üresjárataiban futó feladatok
requestIdleCallback((deadline) => {
// deadline.timeRemaining() megmondja, mennyi időnk van
while (deadline.timeRemaining() > 5 && tasksQueue.length > 0) {
const task = tasksQueue.shift();
task.execute();
}
// Ha maradt feladat, ütemezzük a következő üresjáratra
if (tasksQueue.length > 0) {
requestIdleCallback(processRemainingTasks);
}
}, { timeout: 2000 }); // Maximum 2 mp várakozás
Web Worker-ek: nehéz számítások a háttérszálon
A Web Worker-ek lehetővé teszik, hogy a CPU-intenzív feladatokat a fő száltól teljesen független háttérszálakon futtasd. Ideális megoldás adatfeldolgozáshoz, képmanipulációhoz, keresési indexeléshez — gyakorlatilag mindenhez, ami sok számolással jár, de nincs szüksége DOM-hozzáférésre.
// worker.js - Háttérszálon futó nehéz számítás
self.addEventListener('message', (event) => {
const { type, data } = event.data;
switch (type) {
case 'PROCESS_DATA': {
const result = heavyDataProcessing(data);
self.postMessage({ type: 'RESULT', result });
break;
}
case 'SEARCH': {
const matches = fuzzySearch(data.query, data.items);
self.postMessage({ type: 'SEARCH_RESULT', matches });
break;
}
}
});
function heavyDataProcessing(rawData) {
// Ez a számítás nem blokkolja a fő szálat
return rawData
.filter(item => item.isValid)
.map(item => transform(item))
.sort((a, b) => b.score - a.score);
}
// main.js - A fő szálról indítjuk a worker-t
const worker = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
});
worker.addEventListener('message', (event) => {
const { type, result, matches } = event.data;
if (type === 'RESULT') {
renderResults(result); // A DOM frissítés már a fő szálon
}
if (type === 'SEARCH_RESULT') {
displaySearchResults(matches);
}
});
// Nehéz feldolgozás indítása anélkül, hogy blokkolnánk az UI-t
worker.postMessage({ type: 'PROCESS_DATA', data: largeDataset });
Modulok előtöltése és a hálózati vízesés-effektus elkerülése
A code splitting egyik nem kívánt mellékhatása a hálózati vízesés-effektus (waterfall): a böngésző csak akkor fedezi fel a szükséges chunk-ot, amikor az importáló modul végrehajtódik. Ez kaszkádszerű letöltési láncot eredményezhet, ami pont az ellenkezője annak, amit akartunk.
Szerencsére van rá megoldás.
modulepreload: natív modul előtöltés
A <link rel="modulepreload"> deklaratív módon jelzi a böngészőnek, hogy egy ES modult előre le kell tölteni, elemezni és készen kell tartani. Ez a hagyományos preload-nál hatékonyabb, mert nemcsak letölti, hanem előre feldolgozza a modult és annak függőségeit is.
<!DOCTYPE html>
<html lang="hu">
<head>
<meta charset="UTF-8">
<!-- Fő alkalmazás modul -->
<script type="module" src="/js/app.js"></script>
<!-- Előtölti a kritikus függőségeket párhuzamosan -->
<link rel="modulepreload" href="/js/framework.js">
<link rel="modulepreload" href="/js/router.js">
<link rel="modulepreload" href="/js/home-page.js">
<!-- A kevésbé sürgős chunk-ok normál prefetch-csel -->
<link rel="prefetch" href="/js/analytics-page.js">
<link rel="prefetch" href="/js/settings-page.js">
</head>
<body>
<!-- ... -->
</body>
</html>
Intelligens prefetching: az előrelátó betöltés
Az igazán ügyes megoldás az intelligens prefetching, amely a felhasználó viselkedése alapján próbálja megjósolni, melyik chunk-ra lesz szüksége. Meglepően jól működik egyszerű heurisztikákkal is — például hover események figyelésével.
// Intelligens prefetching - hover alapján
function setupSmartPrefetching() {
const prefetchedUrls = new Set();
document.addEventListener('mouseover', (event) => {
const link = event.target.closest('a[href]');
if (!link) return;
const href = link.getAttribute('href');
if (prefetchedUrls.has(href)) return;
// Csak belső linkekre prefetch-elünk
if (href.startsWith('/') || href.startsWith(window.location.origin)) {
prefetchedUrls.add(href);
const prefetchLink = document.createElement('link');
prefetchLink.rel = 'prefetch';
prefetchLink.href = href;
document.head.appendChild(prefetchLink);
}
});
}
// Intersection Observer alapú prefetching
function setupViewportPrefetching() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const link = entry.target;
const href = link.getAttribute('href');
// Prefetch a chunk-ot, amikor a link a viewport közelébe ér
const prefetchLink = document.createElement('link');
prefetchLink.rel = 'prefetch';
prefetchLink.href = href;
document.head.appendChild(prefetchLink);
observer.unobserve(link);
}
});
}, { rootMargin: '200px' }); // 200px-el a viewport előtt indítjuk
document.querySelectorAll('a[data-prefetch]').forEach(link => {
observer.observe(link);
});
}
Gyakorlati ellenőrzőlista: a JavaScript bundle optimalizálás lépései
Szeretem az ellenőrzőlistákat, mert segítenek, hogy ne felejtsünk el semmit. Az alábbi lista prioritás szerint van rendezve — a legnagyobb hatású technikák vannak elöl.
1. Mérd fel a kiindulási állapotot
- Futtass Lighthouse auditot és jegyezd fel a „Reduce unused JavaScript" értéket
- Használj bundle analyzert (webpack-bundle-analyzer vagy rollup-plugin-visualizer)
- Ellenőrizd a Coverage eszközt a Chrome DevTools-ban a kihasználatlan kód arányáért
- Állíts be teljesítmény költségvetést (performance budget) a build eszközödben
// webpack.config.js - Performance budget
module.exports = {
performance: {
maxEntrypointSize: 250000, // 250 KB
maxAssetSize: 200000, // 200 KB
hints: 'error', // 'warning' helyett 'error': a build elbukik
},
};
// bundlesize csomag használata CI/CD-ben
// package.json
{
"bundlesize": [
{
"path": "./dist/js/main.*.js",
"maxSize": "150 kB",
"compression": "gzip"
},
{
"path": "./dist/js/vendor.*.js",
"maxSize": "100 kB",
"compression": "gzip"
}
]
}
2. Alkalmazd a code splitting-et
- Valósítsd meg az útvonal-alapú code splitting-et
- Azonosítsd és lazy-load-old a nehéz komponenseket (diagramok, editorok, térképek)
- Válaszd külön a vendor kódot az alkalmazáskódtól
- Csoportosítsd a gyakran együtt változó kódot közös chunk-okba
3. Optimalizáld a tree shaking-et
- Használj ES modul szintaxist mindenhol (
import/export) - Állítsd be a
sideEffectsmezőt apackage.json-ban - Kerüld a barrel export fájlokat, vagy használj közvetlen importokat
- Ellenőrizd, hogy a függőségeid támogatják-e a tree shaking-et
4. Csökkentsd a harmadik féltől származó kód mennyiségét
- Auditáld a
node_modulesméretét — van-e felesleges vagy kiváltható függőség? - Cseréld le a nehéz könyvtárakat könnyebb alternatívákra (moment.js → date-fns vagy Intl API, lodash → natív ES metódusok)
- Fontold meg az
import()használatát ritkán használt könyvtárakhoz
5. Optimalizáld a futásidejű teljesítményt
- Bontsd szét a Long Task-okat
scheduler.yield()vagysetTimeouthasználatával - Használj Web Worker-eket CPU-intenzív feladatokhoz
- Alkalmazd a
requestIdleCallback-ot alacsony prioritású feladatokhoz - Használj
modulepreload-ot a kritikus modulok előtöltéséhez
Valós esettanulmány: egy e-kereskedelmi alkalmazás optimalizálása
Hogy mindez ne maradjon puszta elmélet, nézzünk egy valós szcenáriót. Képzeljünk el egy közepes méretű e-kereskedelmi React alkalmazást a következő problémákkal:
- Kezdeti bundle méret: 1,8 MB (gzip előtt), 580 KB (gzip után)
- LCP: 4,2 másodperc mobilon
- INP: 340 ms (messze a 200 ms-os küszöbérték felett)
- Lighthouse Performance pontszám: 42/100
Nem egy szép látvány, ugye?
1. lépés: Bundle analízis
A webpack-bundle-analyzer futtatása után kiderült, hogy a bundle 40%-át három könyvtár teszi ki: moment.js (72 KB gzip), lodash (71 KB gzip), és egy teljes chart.js (60 KB gzip) — amit ráadásul csak egyetlen admin oldalon használnak.
2. lépés: Azonnali nyereségek (quick wins)
// ELŐTTE: Teljes lodash importálása
import _ from 'lodash';
const result = _.debounce(search, 300);
// UTÁNA: Csak a szükséges függvény
import debounce from 'lodash/debounce';
const result = debounce(search, 300);
// ELŐTTE: moment.js (72 KB gzip)
import moment from 'moment';
import 'moment/locale/hu';
const formatted = moment(date).locale('hu').format('YYYY. MMMM DD.');
// UTÁNA: natív Intl API (0 KB - beépített!)
const formatted = new Intl.DateTimeFormat('hu-HU', {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(new Date(date));
3. lépés: Útvonal-alapú code splitting bevezetése
Az adminisztrációs oldal, a kosár, a checkout és a felhasználói profil oldalakat lazy-load-dal töltjük be. A chart.js ezáltal automatikusan elkülönül, hiszen csak az admin oldalon van rá szükség.
4. lépés: A termékkereső INP optimalizálása
A termékkereső szűrőjét Web Worker-be költöztettük, a lista renderelést pedig scheduler.yield()-dal bontottuk szét. Ez önmagában 200 ms-ot javított az INP-n.
Eredmények
- Kezdeti bundle méret: 580 KB → 185 KB gzip (68%-os csökkenés)
- LCP: 4,2 s → 1,8 s mobilon
- INP: 340 ms → 120 ms
- Lighthouse Performance: 42 → 89 pont
A legnagyobb hatású változtatás? A moment.js eltávolítása és az Intl API használata. Egyetlen csere, nulla új függőség, és 72 KB-tal kisebb bundle. Ez jól mutatja, hogy az optimalizálás nem mindig komplex refaktorálást igényel — néha a legegyszerűbb lépések hozzák a legnagyobb eredményt.
A jövő: Islands Architecture és szelektív hidratálás
A hagyományos SPA megközelítés legnagyobb problémája, hogy a teljes oldalt hidratálnia kell (hydration) — vagyis a szerveren renderelt HTML-hez csatolni kell a JavaScript eseménykezelőket és állapotot. Statikus tartalmú oldalakon ez rengeteg felesleges JavaScript feldolgozást jelent.
Az Islands Architecture (sziget-architektúra) radikálisan más megközelítést kínál: csak az interaktív „szigeteket" hidratálja, a statikus tartalom pedig tiszta HTML marad. Az eredmény? Drámaian kevesebb JavaScript és jobb INP.
Az Astro, a Fresh (Deno) és a Qwik keretrendszerek már natívan támogatják ezt a mintát. A 2026-os trendek szerint egyre több keretrendszer mozdul ebbe az irányba — az Angular és a Next.js is kísérletezik a részleges hidratálás különböző formáival.
<!-- Astro példa: csak az interaktív komponensek kapnak JavaScript-et -->
---
import Header from '../components/Header.astro'; // Nulla JS - tiszta HTML
import Footer from '../components/Footer.astro'; // Nulla JS - tiszta HTML
import SearchBar from '../components/SearchBar.tsx'; // React komponens - JS szükséges
import ProductGrid from '../components/ProductGrid.astro'; // Nulla JS
---
<html lang="hu">
<body>
<Header />
<!-- Csak ez a "sziget" kap JavaScript-et -->
<!-- client:visible = akkor hidratál, amikor a viewport-ba kerül -->
<SearchBar client:visible />
<ProductGrid products={products} />
<Footer />
</body>
</html>
Összefoglalás
A JavaScript bundle optimalizálás nem egyszeri feladat, hanem folyamatos gyakorlat. A HTTP Archive adatai évről évre azt mutatják, hogy az oldalak átlagos mérete nő — 2026-ra a medián oldal mérete elérte a 2,9 MB-ot desktopra. Ez a trend csak akkor fordítható meg, ha tudatosan és rendszeresen foglalkozol a bundle méret menedzsmentjével.
A legfontosabb tanulságok:
- Mérj, mielőtt optimalizálsz: Bundle analyzer, Lighthouse és Coverage eszköz nélkül vakrepülés az egész.
- A code splitting az alap: Útvonal-alapú és komponens-szintű kódszétválasztással a felhasználó csak azt tölti le, amire szüksége van.
- A tree shaking nem varázslat: ES modul szintaxis és helyes
sideEffectskonfiguráció nélkül nem fog működni rendesen. - A futásidejű teljesítmény legalább annyira fontos:
scheduler.yield(), Web Worker-ek és a Scheduler API segítségével a fő szál tehermentesíthető. - A modern bundler-ek sokat segítenek: A Vite 8/Rolldown kombináció natív sebességű build-et és kiváló tree shaking-et ad egyben.
- A jövő az Islands Architecture felé mutat: A szelektív hidratálás drasztikusan csökkenti a szükséges JavaScript mennyiségét.
Állíts be teljesítmény költségvetést, integráld a bundle méret ellenőrzést a CI/CD pipeline-odba, és rendszeresen auditáld a függőségeidet. A felhasználóid — és a keresőoptimalizálás — hálás lesz érte.