Lazy loading to strategia optymalizacji polegająca na opóźnieniu ładowania zasobów do momentu, gdy stają się rzeczywiście potrzebne. Zamiast ładować wszystko od razu podczas pierwszego renderowania strony, ładujemy elementy "na żądanie" - gdy użytkownik przewija stronę, przechodzi na nową trasę, lub wykonuje akcję wymagającą określonego komponentu.
1<!-- Natywny lazy loading (wspierany przez nowoczesne przeglądarki) -->
2<img src="hero-image.jpg" alt="Obraz główny" loading="lazy" />
3
4<!-- Eager loading dla obrazów above-the-fold -->
5<img src="logo.jpg" alt="Logo" loading="eager" />
6
7<!-- Auto (domyślne zachowanie przeglądarki) -->
8<img src="banner.jpg" alt="Banner" loading="auto" />1// Implementacja lazy loading z Intersection Observer
2class LazyImageLoader {
3 constructor() {
4 this.imageObserver = new IntersectionObserver(
5 this.handleIntersection.bind(this),
6 {
7 root: null, // viewport
8 rootMargin: '50px', // ładuj 50px przed wejściem w viewport
9 threshold: 0.1 // trigger gdy 10% obrazu jest widoczne
10 }
11 );
12
13 this.initLazyImages();
14 }
15
16 initLazyImages() {
17 const lazyImages = document.querySelectorAll('img[data-src]');
18 lazyImages.forEach(img => {
19 this.imageObserver.observe(img);
20 });
21 }
22
23 handleIntersection(entries) {
24 entries.forEach(entry => {
25 if (entry.isIntersecting) {
26 const img = entry.target;
27 this.loadImage(img);
28 this.imageObserver.unobserve(img);
29 }
30 });
31 }
32
33 loadImage(img) {
34 // Ładowanie obrazu z atrybutu data-src
35 img.src = img.dataset.src;
36 img.classList.add('loaded');
37
38 // Usuwanie data-src po załadowaniu
39 delete img.dataset.src;
40
41 // Obsługa błędów ładowania
42 img.onerror = () => {
43 img.src = '/images/error-placeholder.png';
44 img.classList.add('error');
45 };
46 }
47}
48
49// Inicjalizacja
50document.addEventListener('DOMContentLoaded', () => {
51 new LazyImageLoader();
52});1// Hook dla lazy loading obrazów
2import { useState, useRef, useEffect } from 'react';
3
4function useIntersectionObserver(options = {}) {
5 const [isIntersecting, setIsIntersecting] = useState(false);
6 const [hasIntersected, setHasIntersected] = useState(false);
7 const elementRef = useRef(null);
8
9 useEffect(() => {
10 const element = elementRef.current;
11 if (!element) return;
12
13 const observer = new IntersectionObserver(
14 ([entry]) => {
15 const isElementIntersecting = entry.isIntersecting;
16 setIsIntersecting(isElementIntersecting);
17
18 if (isElementIntersecting && !hasIntersected) {
19 setHasIntersected(true);
20 }
21 },
22 { threshold: 0.1, rootMargin: '50px', ...options }
23 );
24
25 observer.observe(element);
26
27 return () => observer.disconnect();
28 }, [hasIntersected, options]);
29
30 return { elementRef, isIntersecting, hasIntersected };
31}
32
33// Komponent LazyImage
34function LazyImage({ src, alt, placeholder, className = '' }) {
35 const [imageLoaded, setImageLoaded] = useState(false);
36 const [imageError, setImageError] = useState(false);
37 const { elementRef, hasIntersected } = useIntersectionObserver();
38
39 const handleImageLoad = () => {
40 setImageLoaded(true);
41 };
42
43 const handleImageError = () => {
44 setImageError(true);
45 };
46
47 return (
48 <div ref={elementRef} className={`lazy-image-container ${className}`}>
49 {/* Placeholder wyświetlany podczas ładowania */}
50 {!imageLoaded && (
51 <img
52 src={placeholder}
53 alt={`${alt} placeholder`}
54 className="placeholder-image"
55 />
56 )}
57
58 {/* Rzeczywisty obraz ładowany lazy */}
59 {hasIntersected && (
60 <img
61 src={imageError ? '/images/error.png' : src}
62 alt={alt}
63 onLoad={handleImageLoad}
64 onError={handleImageError}
65 className={`lazy-image ${imageLoaded ? 'loaded' : 'loading'}`}
66 />
67 )}
68
69 {/* Loading spinner */}
70 {hasIntersected && !imageLoaded && !imageError && (
71 <div className="image-spinner">Ładowanie...</div>
72 )}
73 </div>
74 );
75}
76
77// Użycie komponentu
78function Gallery({ images }) {
79 return (
80 <div className="gallery">
81 {images.map((image, index) => (
82 <LazyImage
83 key={index}
84 src={image.url}
85 alt={image.alt}
86 placeholder={image.placeholder}
87 className="gallery-item"
88 />
89 ))}
90 </div>
91 );
92}1// Komponent z progresywnym ładowaniem (blur → sharp)
2function ProgressiveImage({ src, placeholder, alt }) {
3 const [imageLoaded, setImageLoaded] = useState(false);
4 const { elementRef, hasIntersected } = useIntersectionObserver();
5
6 return (
7 <div ref={elementRef} className="progressive-image">
8 {/* Rozmyty placeholder */}
9 <img
10 src={placeholder}
11 alt={alt}
12 className={`progressive-placeholder`}
13 style={{
14 filter: 'blur(10px)',
15 transition: 'opacity 0.3s ease'
16 }}
17 />
18
19 {/* Ostry obraz */}
20 {hasIntersected && (
21 <img
22 src={src}
23 alt={alt}
24 onLoad={() => setImageLoaded(true)}
25 className={`main-image ${imageLoaded ? 'visible' : 'hidden'}`}
26 style={{
27 opacity: imageLoaded ? 1 : 0,
28 transition: 'opacity 0.3s ease'
29 }}
30 />
31 )}
32 </div>
33 );
34}1import { Suspense, lazy } from 'react';
2import { Routes, Route } from 'react-router-dom';
3import ErrorBoundary from './components/ErrorBoundary';
4import LoadingSpinner from './components/LoadingSpinner';
5
6// Lazy loading komponentów tras
7const HomePage = lazy(() => import('./pages/HomePage'));
8const ProductsPage = lazy(() => import('./pages/ProductsPage'));
9const ProductDetailPage = lazy(() => import('./pages/ProductDetailPage'));
10const UserProfilePage = lazy(() => import('./pages/UserProfilePage'));
11const AdminPanel = lazy(() => import('./pages/AdminPanel'));
12
13// Komponenty z custom loading i error handling
14const LazyRoute = ({ children }) => (
15 <ErrorBoundary>
16 <Suspense fallback={<LoadingSpinner />}>
17 {children}
18 </Suspense>
19 </ErrorBoundary>
20);
21
22function App() {
23 return (
24 <Routes>
25 <Route
26 path="/"
27 element={
28 <LazyRoute>
29 <HomePage />
30 </LazyRoute>
31 }
32 />
33 <Route
34 path="/products"
35 element={
36 <LazyRoute>
37 <ProductsPage />
38 </LazyRoute>
39 }
40 />
41 <Route
42 path="/products/:id"
43 element={
44 <LazyRoute>
45 <ProductDetailPage />
46 </LazyRoute>
47 }
48 />
49 <Route
50 path="/profile"
51 element={
52 <LazyRoute>
53 <UserProfilePage />
54 </LazyRoute>
55 }
56 />
57 <Route
58 path="/admin/*"
59 element={
60 <LazyRoute>
61 <AdminPanel />
62 </LazyRoute>
63 }
64 />
65 </Routes>
66 );
67}1// Preloading tras na podstawie user behavior
2class RoutePreloader {
3 constructor() {
4 this.preloadedRoutes = new Set();
5 this.setupPreloading();
6 }
7
8 setupPreloading() {
9 // Preload na hover
10 this.setupHoverPreloading();
11
12 // Preload na podstawie prawdopodobieństwa
13 this.setupPredictivePreloading();
14
15 // Preload w idle time
16 this.setupIdlePreloading();
17 }
18
19 setupHoverPreloading() {
20 document.addEventListener('mouseover', (e) => {
21 const link = e.target.closest('a[href]');
22 if (link && this.shouldPreload(link.href)) {
23 this.preloadRoute(link.href);
24 }
25 });
26 }
27
28 setupPredictivePreloading() {
29 // Jeśli użytkownik jest na /products, preload /products/:id
30 const currentPath = window.location.pathname;
31
32 if (currentPath === '/products') {
33 // Delay żeby nie wpływać na current page load
34 setTimeout(() => {
35 this.preloadRoute('/products/popular');
36 }, 2000);
37 }
38 }
39
40 setupIdlePreloading() {
41 if ('requestIdleCallback' in window) {
42 requestIdleCallback(() => {
43 this.preloadCriticalRoutes();
44 });
45 }
46 }
47
48 preloadRoute(path) {
49 if (this.preloadedRoutes.has(path)) return;
50
51 const routeMap = {
52 '/products': () => import('./pages/ProductsPage'),
53 '/profile': () => import('./pages/UserProfilePage'),
54 '/admin': () => import('./pages/AdminPanel')
55 };
56
57 const preloadFunction = routeMap[path];
58 if (preloadFunction) {
59 preloadFunction().then(() => {
60 this.preloadedRoutes.add(path);
61 console.log(`Route ${path} preloaded`);
62 });
63 }
64 }
65
66 shouldPreload(href) {
67 // Preload tylko internal links
68 return href.startsWith(window.location.origin);
69 }
70
71 preloadCriticalRoutes() {
72 const critical = ['/products', '/profile'];
73 critical.forEach(route => this.preloadRoute(route));
74 }
75}
76
77// Inicjalizacja
78new RoutePreloader();1// Lazy loading drogich komponentów
2const ExpensiveChart = lazy(() => import('./components/ExpensiveChart'));
3const DataVisualization = lazy(() => import('./components/DataVisualization'));
4const VideoPlayer = lazy(() => import('./components/VideoPlayer'));
5const CodeEditor = lazy(() => import('./components/CodeEditor'));
6
7function Dashboard({ activeTab, data }) {
8 return (
9 <div className="dashboard">
10 <nav className="dashboard-nav">
11 {/* Nawigacja zawsze widoczna */}
12 </nav>
13
14 <main className="dashboard-content">
15 {activeTab === 'charts' && (
16 <Suspense fallback={<div>Ładowanie wykresów...</div>}>
17 <ExpensiveChart data={data} />
18 </Suspense>
19 )}
20
21 {activeTab === 'visualization' && (
22 <Suspense fallback={<div>Ładowanie wizualizacji...</div>}>
23 <DataVisualization data={data} />
24 </Suspense>
25 )}
26
27 {activeTab === 'video' && (
28 <Suspense fallback={<div>Ładowanie odtwarzacza...</div>}>
29 <VideoPlayer />
30 </Suspense>
31 )}
32
33 {activeTab === 'editor' && (
34 <Suspense fallback={<div>Ładowanie edytora...</div>}>
35 <CodeEditor />
36 </Suspense>
37 )}
38 </main>
39 </div>
40 );
41}1// Hook dla lazy modali
2function useLazyModal() {
3 const [isOpen, setIsOpen] = useState(false);
4 const [Component, setComponent] = useState(null);
5
6 const openModal = async (modalType) => {
7 const modalMap = {
8 'user-settings': () => import('./modals/UserSettingsModal'),
9 'payment': () => import('./modals/PaymentModal'),
10 'image-editor': () => import('./modals/ImageEditorModal'),
11 };
12
13 const importFunction = modalMap[modalType];
14 if (importFunction) {
15 const { default: ModalComponent } = await importFunction();
16 setComponent(() => ModalComponent);
17 setIsOpen(true);
18 }
19 };
20
21 const closeModal = () => {
22 setIsOpen(false);
23 // Opcjonalnie: unload komponentu po zamknięciu
24 setTimeout(() => setComponent(null), 300);
25 };
26
27 return { Component, isOpen, openModal, closeModal };
28}
29
30// Użycie lazy modali
31function App() {
32 const { Component: ModalComponent, isOpen, openModal, closeModal } = useLazyModal();
33
34 return (
35 <div>
36 <button onClick={() => openModal('user-settings')}>
37 Ustawienia użytkownika
38 </button>
39 <button onClick={() => openModal('payment')}>
40 Płatność
41 </button>
42 <button onClick={() => openModal('image-editor')}>
43 Edytor obrazów
44 </button>
45
46 {isOpen && ModalComponent && (
47 <Suspense fallback={<div>Ładowanie...</div>}>
48 <ModalComponent onClose={closeModal} />
49 </Suspense>
50 )}
51 </div>
52 );
53}1// Lazy loading całych feature'ów
2const AdminFeature = lazy(() => import('./features/Admin'));
3const AnalyticsFeature = lazy(() => import('./features/Analytics'));
4const ShoppingCartFeature = lazy(() => import('./features/ShoppingCart'));
5
6function FeatureLoader({ feature, ...props }) {
7 const featureMap = {
8 admin: AdminFeature,
9 analytics: AnalyticsFeature,
10 cart: ShoppingCartFeature
11 };
12
13 const FeatureComponent = featureMap[feature];
14
15 if (!FeatureComponent) {
16 return <div>Nieznana funkcjonalność: {feature}</div>;
17 }
18
19 return (
20 <ErrorBoundary>
21 <Suspense fallback={<FeatureLoadingSkeleton feature={feature} />}>
22 <FeatureComponent {...props} />
23 </Suspense>
24 </ErrorBoundary>
25 );
26}
27
28// Skeleton dla różnych feature'ów
29function FeatureLoadingSkeleton({ feature }) {
30 const skeletons = {
31 admin: <AdminSkeleton />,
32 analytics: <ChartsSkeleton />,
33 cart: <CartSkeleton />
34 };
35
36 return skeletons[feature] || <DefaultSkeleton />;
37}1// Virtual scrolling dla dużych list
2import { FixedSizeList as List } from 'react-window';
3
4function VirtualizedList({ items, itemHeight = 50 }) {
5 const [loadedItems, setLoadedItems] = useState(new Set());
6
7 const loadItem = useCallback(async (index) => {
8 if (loadedItems.has(index)) return;
9
10 // Symulacja ładowania danych
11 await new Promise(resolve => setTimeout(resolve, 100));
12 setLoadedItems(prev => new Set([...prev, index]));
13 }, [loadedItems]);
14
15 const Row = ({ index, style }) => {
16 const item = items[index];
17 const isLoaded = loadedItems.has(index);
18
19 useEffect(() => {
20 if (!isLoaded) {
21 loadItem(index);
22 }
23 }, [index, isLoaded, loadItem]);
24
25 return (
26 <div style={style} className="list-item">
27 {isLoaded ? (
28 <ItemContent item={item} />
29 ) : (
30 <ItemSkeleton />
31 )}
32 </div>
33 );
34 };
35
36 return (
37 <List
38 height={400}
39 itemCount={items.length}
40 itemSize={itemHeight}
41 overscanCount={5} // Preload 5 items poza viewport
42 >
43 {Row}
44 </List>
45 );
46}1// Cache dla lazy-loaded komponentów
2class ComponentCache {
3 constructor() {
4 this.cache = new Map();
5 }
6
7 async get(key, loader) {
8 if (this.cache.has(key)) {
9 return this.cache.get(key);
10 }
11
12 const component = await loader();
13 this.cache.set(key, component);
14 return component;
15 }
16
17 preload(key, loader) {
18 if (!this.cache.has(key)) {
19 this.get(key, loader);
20 }
21 }
22
23 clear(key) {
24 if (key) {
25 this.cache.delete(key);
26 } else {
27 this.cache.clear();
28 }
29 }
30}
31
32const componentCache = new ComponentCache();
33
34// Hook wykorzystujący cache
35function useLazyComponent(componentKey, loader) {
36 const [Component, setComponent] = useState(null);
37 const [loading, setLoading] = useState(false);
38 const [error, setError] = useState(null);
39
40 const loadComponent = useCallback(async () => {
41 if (Component) return;
42
43 setLoading(true);
44 setError(null);
45
46 try {
47 const loadedComponent = await componentCache.get(componentKey, loader);
48 setComponent(() => loadedComponent.default);
49 } catch (err) {
50 setError(err);
51 } finally {
52 setLoading(false);
53 }
54 }, [componentKey, loader, Component]);
55
56 // Preload funkcja
57 const preload = useCallback(() => {
58 componentCache.preload(componentKey, loader);
59 }, [componentKey, loader]);
60
61 return { Component, loading, error, loadComponent, preload };
62}1// Adaptacyjne ładowanie na podstawie połączenia
2function useConnectionAwareLazyLoading() {
3 const [connectionType, setConnectionType] = useState('4g');
4
5 useEffect(() => {
6 const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
7
8 if (connection) {
9 setConnectionType(connection.effectiveType);
10
11 const updateConnection = () => {
12 setConnectionType(connection.effectiveType);
13 };
14
15 connection.addEventListener('change', updateConnection);
16 return () => connection.removeEventListener('change', updateConnection);
17 }
18 }, []);
19
20 const shouldLazyLoad = useCallback((priority = 'normal') => {
21 const strategies = {
22 '4g': { immediate: true, normal: true, low: true },
23 '3g': { immediate: true, normal: true, low: false },
24 '2g': { immediate: true, normal: false, low: false },
25 'slow-2g': { immediate: false, normal: false, low: false }
26 };
27
28 return strategies[connectionType]?.[priority] ?? false;
29 }, [connectionType]);
30
31 const getLoadingStrategy = useCallback(() => {
32 const strategies = {
33 '4g': 'aggressive', // Ładuj wszystko
34 '3g': 'moderate', // Ładuj tylko gdy potrzebne
35 '2g': 'conservative', // Ładuj minimalnie
36 'slow-2g': 'minimal' // Tylko niezbędne
37 };
38
39 return strategies[connectionType] || 'moderate';
40 }, [connectionType]);
41
42 return { connectionType, shouldLazyLoad, getLoadingStrategy };
43}
44
45// Komponent adaptujący się do połączenia
46function AdaptiveImageGrid({ images }) {
47 const { shouldLazyLoad, getLoadingStrategy } = useConnectionAwareLazyLoading();
48 const strategy = getLoadingStrategy();
49
50 const imageSettings = {
51 aggressive: { quality: 'high', eager: 10 },
52 moderate: { quality: 'medium', eager: 4 },
53 conservative: { quality: 'low', eager: 2 },
54 minimal: { quality: 'thumbnail', eager: 1 }
55 };
56
57 const settings = imageSettings[strategy];
58
59 return (
60 <div className="image-grid">
61 {images.map((image, index) => (
62 <AdaptiveImage
63 key={image.id}
64 src={image.url}
65 quality={settings.quality}
66 placeholder={image.placeholder}
67 loading={index < settings.eager ? 'eager' : 'lazy'}
68 shouldOptimize={strategy !== 'aggressive'}
69 />
70 ))}
71 </div>
72 );
73}1// Monitoring wydajności lazy loading
2class LazyLoadingMonitor {
3 constructor() {
4 this.metrics = {
5 componentsLoaded: 0,
6 totalLoadTime: 0,
7 failedLoads: 0,
8 cacheHits: 0
9 };
10
11 this.startTime = performance.now();
12 }
13
14 trackComponentLoad(componentName, loadTime, fromCache = false) {
15 this.metrics.componentsLoaded++;
16 this.metrics.totalLoadTime += loadTime;
17
18 if (fromCache) {
19 this.metrics.cacheHits++;
20 }
21
22 // Web Vitals tracking
23 if (loadTime > 2500) { // Slow loading threshold
24 console.warn(`Slow lazy load detected: ${componentName} took ${loadTime}ms`);
25 }
26
27 // Send to analytics
28 this.sendMetrics('component_lazy_load', {
29 component: componentName,
30 loadTime,
31 fromCache,
32 timestamp: Date.now()
33 });
34 }
35
36 trackFailedLoad(componentName, error) {
37 this.metrics.failedLoads++;
38
39 console.error(`Failed to lazy load ${componentName}:`, error);
40
41 this.sendMetrics('component_lazy_load_error', {
42 component: componentName,
43 error: error.message,
44 timestamp: Date.now()
45 });
46 }
47
48 getAverageLoadTime() {
49 return this.metrics.componentsLoaded > 0
50 ? this.metrics.totalLoadTime / this.metrics.componentsLoaded
51 : 0;
52 }
53
54 getCacheHitRate() {
55 return this.metrics.componentsLoaded > 0
56 ? (this.metrics.cacheHits / this.metrics.componentsLoaded) * 100
57 : 0;
58 }
59
60 sendMetrics(event, data) {
61 // Integracja z Google Analytics, Mixpanel, itp.
62 if (typeof gtag !== 'undefined') {
63 gtag('event', event, data);
64 }
65 }
66
67 generateReport() {
68 const sessionTime = performance.now() - this.startTime;
69
70 return {
71 sessionDuration: sessionTime,
72 componentsLoaded: this.metrics.componentsLoaded,
73 averageLoadTime: this.getAverageLoadTime(),
74 cacheHitRate: this.getCacheHitRate(),
75 failureRate: (this.metrics.failedLoads / this.metrics.componentsLoaded) * 100,
76 totalFailures: this.metrics.failedLoads
77 };
78 }
79}
80
81// Singleton instance
82const lazyLoadMonitor = new LazyLoadingMonitor();
83
84// Hook z monitoringiem
85function useMonitoredLazyComponent(componentName, loader) {
86 const [Component, setComponent] = useState(null);
87 const [loading, setLoading] = useState(false);
88
89 const loadComponent = useCallback(async () => {
90 if (Component) return;
91
92 const startTime = performance.now();
93 setLoading(true);
94
95 try {
96 const loadedComponent = await loader();
97 const loadTime = performance.now() - startTime;
98
99 setComponent(() => loadedComponent.default);
100 lazyLoadMonitor.trackComponentLoad(componentName, loadTime);
101 } catch (error) {
102 lazyLoadMonitor.trackFailedLoad(componentName, error);
103 throw error;
104 } finally {
105 setLoading(false);
106 }
107 }, [componentName, loader, Component]);
108
109 return { Component, loading, loadComponent };
110}1// Above-the-fold content - eager loading
2// Below-the-fold content - lazy loading
3function ContentStrategy() {
4 return (
5 <>
6 {/* Critical path - eager */}
7 <Header />
8 <Hero />
9 <MainContent />
10
11 {/* Secondary content - lazy */}
12 <Suspense fallback={<SectionSkeleton />}>
13 <LazySection name="testimonials" />
14 </Suspense>
15
16 <Suspense fallback={<SectionSkeleton />}>
17 <LazySection name="newsletter" />
18 </Suspense>
19
20 {/* Low priority - very lazy */}
21 <Suspense fallback={<FooterSkeleton />}>
22 <LazyFooter />
23 </Suspense>
24 </>
25 );
26}1// Zawsze zapewnij graceful fallbacks
2function LazyComponentWrapper({ children, fallback, errorFallback }) {
3 return (
4 <ErrorBoundary
5 fallback={errorFallback || <div>Wystąpił błąd podczas ładowania</div>}
6 >
7 <Suspense fallback={fallback || <LoadingSkeleton />}>
8 {children}
9 </Suspense>
10 </ErrorBoundary>
11 );
12}1// Strategiczne preloadowanie na podstawie user behavior
2function useStrategicPreloading() {
3 useEffect(() => {
4 // Preload po 2 sekundach bezaktywności
5 const timeoutId = setTimeout(() => {
6 // Preload prawdopodobnych następnych komponentów
7 import('./components/UserProfile');
8 import('./components/ShoppingCart');
9 }, 2000);
10
11 // Preload na podstawie mouse movement w kierunku linka
12 const handleMouseMove = (e) => {
13 // Jeśli kursor zbliża się do określonego elementu
14 // preload związany komponent
15 };
16
17 document.addEventListener('mousemove', handleMouseMove);
18
19 return () => {
20 clearTimeout(timeoutId);
21 document.removeEventListener('mousemove', handleMouseMove);
22 };
23 }, []);
24}Lazy loading to potężna technika optymalizacji, która:
Kluczem do sukcesu jest zbalansowanie między wydajnością a doświadczeniem użytkownika - lazy loading powinien być niewidoczny dla użytkownika, a jednocześnie znacząco poprawiać wydajność aplikacji.