`n

Lazy Loading Implementation - Complete Guide

Published: September 25, 2024 | Reading time: 17 minutes

Lazy Loading Overview

Lazy loading improves performance by loading content only when needed:

Lazy Loading Benefits
# Lazy Loading Benefits
- Faster initial page load
- Reduced bandwidth usage
- Better user experience
- Improved Core Web Vitals
- Lower server load
- Enhanced mobile performance
- Better SEO rankings

Native Image Lazy Loading

HTML Lazy Loading Attribute

Native Lazy Loading Implementation
# Native Image Lazy Loading

# 1. Basic Lazy Loading
Description

# 2. Lazy Loading with Placeholder
Description

# 3. Responsive Images with Lazy Loading

    
    
    Description


# 4. CSS for Lazy Loading States
.lazy-image {
    opacity: 0;
    transition: opacity 0.3s ease-in-out;
}

.lazy-image.loaded {
    opacity: 1;
}

.lazy-image.loading {
    background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
    background-size: 200% 100%;
    animation: loading 1.5s infinite;
}

@keyframes loading {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

# 5. JavaScript Enhancement for Native Lazy Loading
document.addEventListener('DOMContentLoaded', function() {
    const lazyImages = document.querySelectorAll('img[loading="lazy"]');
    
    lazyImages.forEach(img => {
        img.addEventListener('load', function() {
            this.classList.add('loaded');
        });
        
        img.addEventListener('error', function() {
            this.src = 'placeholder-error.jpg';
            this.classList.add('error');
        });
    });
});

# 6. Intersection Observer for Enhanced Control
class LazyImageLoader {
    constructor(options = {}) {
        this.options = {
            root: null,
            rootMargin: '50px',
            threshold: 0.1,
            ...options
        };
        
        this.observer = new IntersectionObserver(
            this.handleIntersection.bind(this),
            this.options
        );
    }
    
    handleIntersection(entries) {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                this.loadImage(entry.target);
                this.observer.unobserve(entry.target);
            }
        });
    }
    
    loadImage(img) {
        const src = img.dataset.src;
        if (src) {
            img.src = src;
            img.classList.add('loading');
            
            img.addEventListener('load', () => {
                img.classList.remove('loading');
                img.classList.add('loaded');
            });
            
            img.addEventListener('error', () => {
                img.classList.remove('loading');
                img.classList.add('error');
                img.src = 'placeholder-error.jpg';
            });
        }
    }
    
    observe(images) {
        images.forEach(img => this.observer.observe(img));
    }
    
    disconnect() {
        this.observer.disconnect();
    }
}

# Initialize lazy loading
const lazyLoader = new LazyImageLoader();
const lazyImages = document.querySelectorAll('img[data-src]');
lazyLoader.observe(lazyImages);

Component Lazy Loading

React Lazy Loading

React Lazy Loading Implementation
# React Component Lazy Loading

# 1. React.lazy() Implementation
import React, { Suspense, lazy } from 'react';

const LazyComponent = lazy(() => import('./HeavyComponent'));

function App() {
    return (
        

My App

Loading component...
}>
); } # 2. Conditional Lazy Loading import React, { useState, Suspense, lazy } from 'react'; const LazyChart = lazy(() => import('./Chart')); const LazyTable = lazy(() => import('./Table')); function Dashboard() { const [showChart, setShowChart] = useState(false); const [showTable, setShowTable] = useState(false); return (
{showChart && ( Loading chart...
}> )} {showTable && ( Loading table...
}> )}
); } # 3. Intersection Observer Hook import { useEffect, useRef, useState } from 'react'; function useIntersectionObserver(options = {}) { const [isIntersecting, setIsIntersecting] = useState(false); const [hasIntersected, setHasIntersected] = useState(false); const ref = useRef(); useEffect(() => { const element = ref.current; if (!element) return; const observer = new IntersectionObserver( ([entry]) => { setIsIntersecting(entry.isIntersecting); if (entry.isIntersecting && !hasIntersected) { setHasIntersected(true); } }, { threshold: 0.1, rootMargin: '50px', ...options } ); observer.observe(element); return () => { observer.unobserve(element); }; }, [hasIntersected, options]); return [ref, isIntersecting, hasIntersected]; } # 4. Lazy Component with Intersection Observer import React, { Suspense, lazy } from 'react'; const LazyComponent = lazy(() => import('./HeavyComponent')); function LazyWrapper({ children, fallback =
Loading...
}) { const [ref, isIntersecting, hasIntersected] = useIntersectionObserver(); return (
{hasIntersected ? ( {children} ) : (
Scroll to load component
)}
); } # Usage function App() { return (

My App

); } # 5. Advanced Lazy Loading Hook import { useEffect, useRef, useState, useCallback } from 'react'; function useLazyLoad(importFunc, options = {}) { const [component, setComponent] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [hasLoaded, setHasLoaded] = useState(false); const ref = useRef(); const loadComponent = useCallback(async () => { if (hasLoaded || loading) return; setLoading(true); setError(null); try { const module = await importFunc(); setComponent(() => module.default); setHasLoaded(true); } catch (err) { setError(err); } finally { setLoading(false); } }, [importFunc, hasLoaded, loading]); useEffect(() => { const element = ref.current; if (!element) return; const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting && !hasLoaded) { loadComponent(); } }, { threshold: 0.1, rootMargin: '50px', ...options } ); observer.observe(element); return () => { observer.unobserve(element); }; }, [loadComponent, hasLoaded, options]); return { ref, component, loading, error, hasLoaded }; } # Usage function MyComponent() { const { ref, component: Chart, loading, error } = useLazyLoad( () => import('./Chart') ); return (
{loading &&
Loading chart...
} {error &&
Error loading chart: {error.message}
} {Chart && }
); } # 6. Lazy Loading with Error Boundary import React, { Suspense, lazy } from 'react'; const LazyComponent = lazy(() => import('./HeavyComponent')); class LazyErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error) { return { hasError: true, error }; } componentDidCatch(error, errorInfo) { console.error('Lazy loading error:', error, errorInfo); } render() { if (this.state.hasError) { return (

Failed to load component

); } return this.props.children; } } function App() { return ( Loading...}> ); }

Vue.js Lazy Loading

Vue-Specific Lazy Loading

Vue.js Lazy Loading Implementation
# Vue.js Lazy Loading Strategies

# 1. Vue Router Lazy Loading
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
    {
        path: '/',
        name: 'Home',
        component: () => import('../views/Home.vue')
    },
    {
        path: '/about',
        name: 'About',
        component: () => import('../views/About.vue')
    },
    {
        path: '/contact',
        name: 'Contact',
        component: () => import('../views/Contact.vue')
    }
];

const router = createRouter({
    history: createWebHistory(),
    routes
});

export default router;

# 2. Vue Component Lazy Loading




# 3. Vue Intersection Observer Directive
// directives/lazy-load.js
export default {
    mounted(el, binding) {
        const { value: importFunc, modifiers } = binding;
        
        const observer = new IntersectionObserver(
            (entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        importFunc().then(module => {
                            el.innerHTML = '';
                            el.appendChild(module.default);
                        });
                        observer.unobserve(el);
                    }
                });
            },
            {
                threshold: 0.1,
                rootMargin: '50px'
            }
        );
        
        observer.observe(el);
    }
};

# Usage in template
Loading...
# 4. Vue Composition API Lazy Loading import { ref, onMounted, onUnmounted } from 'vue'; export function useLazyLoad(importFunc, options = {}) { const element = ref(null); const component = ref(null); const loading = ref(false); const error = ref(null); const hasLoaded = ref(false); let observer = null; const loadComponent = async () => { if (hasLoaded.value || loading.value) return; loading.value = true; error.value = null; try { const module = await importFunc(); component.value = module.default; hasLoaded.value = true; } catch (err) { error.value = err; } finally { loading.value = false; } }; onMounted(() => { if (!element.value) return; observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting && !hasLoaded.value) { loadComponent(); } }, { threshold: 0.1, rootMargin: '50px', ...options } ); observer.observe(element.value); }); onUnmounted(() => { if (observer) { observer.disconnect(); } }); return { element, component, loading, error, hasLoaded }; } # Usage in component # 5. Vue Image Lazy Loading Component # 6. Vue Lazy Loading Plugin // plugins/lazy-loading.js export default { install(app, options = {}) { const { threshold = 0.1, rootMargin = '50px' } = options; app.directive('lazy', { mounted(el, binding) { const { value: importFunc } = binding; const observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { importFunc().then(module => { if (module.default) { el.innerHTML = ''; el.appendChild(module.default); } }); observer.unobserve(el); } }); }, { threshold, rootMargin } ); observer.observe(el); } }); } }; # Usage import { createApp } from 'vue'; import LazyLoadingPlugin from './plugins/lazy-loading'; const app = createApp(App); app.use(LazyLoadingPlugin, { threshold: 0.1, rootMargin: '50px' });

Advanced Lazy Loading

Performance Optimizations

Advanced Lazy Loading Techniques
# Advanced Lazy Loading Techniques

# 1. Preloading Strategy
class LazyLoadManager {
    constructor(options = {}) {
        this.options = {
            threshold: 0.1,
            rootMargin: '50px',
            preloadDistance: 100,
            ...options
        };
        
        this.observer = new IntersectionObserver(
            this.handleIntersection.bind(this),
            this.options
        );
        
        this.preloadObserver = new IntersectionObserver(
            this.handlePreload.bind(this),
            {
                threshold: 0,
                rootMargin: `${this.options.preloadDistance}px`
            }
        );
        
        this.loadedElements = new Set();
        this.preloadedElements = new Set();
    }
    
    handleIntersection(entries) {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                this.loadElement(entry.target);
                this.observer.unobserve(entry.target);
            }
        });
    }
    
    handlePreload(entries) {
        entries.forEach(entry => {
            if (entry.isIntersecting && !this.preloadedElements.has(entry.target)) {
                this.preloadElement(entry.target);
                this.preloadedElements.add(entry.target);
            }
        });
    }
    
    async loadElement(element) {
        if (this.loadedElements.has(element)) return;
        
        const importFunc = element.dataset.lazyLoad;
        if (!importFunc) return;
        
        try {
            const module = await eval(`(${importFunc})()`);
            element.innerHTML = '';
            element.appendChild(module.default);
            this.loadedElements.add(element);
        } catch (error) {
            console.error('Failed to load element:', error);
        }
    }
    
    async preloadElement(element) {
        const importFunc = element.dataset.lazyLoad;
        if (!importFunc) return;
        
        try {
            // Preload without executing
            await eval(`(${importFunc})()`);
        } catch (error) {
            console.error('Failed to preload element:', error);
        }
    }
    
    observe(element) {
        this.observer.observe(element);
        this.preloadObserver.observe(element);
    }
    
    disconnect() {
        this.observer.disconnect();
        this.preloadObserver.disconnect();
    }
}

# 2. Lazy Loading with Caching
class CachedLazyLoader {
    constructor() {
        this.cache = new Map();
        this.loadingPromises = new Map();
        this.observer = new IntersectionObserver(
            this.handleIntersection.bind(this),
            { threshold: 0.1, rootMargin: '50px' }
        );
    }
    
    handleIntersection(entries) {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                this.loadElement(entry.target);
                this.observer.unobserve(entry.target);
            }
        });
    }
    
    async loadElement(element) {
        const importPath = element.dataset.lazyLoad;
        if (!importPath) return;
        
        // Check cache first
        if (this.cache.has(importPath)) {
            this.renderElement(element, this.cache.get(importPath));
            return;
        }
        
        // Check if already loading
        if (this.loadingPromises.has(importPath)) {
            const module = await this.loadingPromises.get(importPath);
            this.renderElement(element, module);
            return;
        }
        
        // Start loading
        const loadingPromise = this.loadModule(importPath);
        this.loadingPromises.set(importPath, loadingPromise);
        
        try {
            const module = await loadingPromise;
            this.cache.set(importPath, module);
            this.loadingPromises.delete(importPath);
            this.renderElement(element, module);
        } catch (error) {
            this.loadingPromises.delete(importPath);
            console.error('Failed to load module:', error);
        }
    }
    
    async loadModule(importPath) {
        const module = await import(importPath);
        return module.default;
    }
    
    renderElement(element, module) {
        element.innerHTML = '';
        element.appendChild(module);
    }
    
    observe(element) {
        this.observer.observe(element);
    }
    
    disconnect() {
        this.observer.disconnect();
    }
}

# 3. Lazy Loading with Error Recovery
class ResilientLazyLoader {
    constructor(options = {}) {
        this.options = {
            maxRetries: 3,
            retryDelay: 1000,
            fallbackComponent: null,
            ...options
        };
        
        this.retryCount = new Map();
        this.observer = new IntersectionObserver(
            this.handleIntersection.bind(this),
            { threshold: 0.1, rootMargin: '50px' }
        );
    }
    
    handleIntersection(entries) {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                this.loadElement(entry.target);
                this.observer.unobserve(entry.target);
            }
        });
    }
    
    async loadElement(element) {
        const importPath = element.dataset.lazyLoad;
        if (!importPath) return;
        
        const retryCount = this.retryCount.get(importPath) || 0;
        
        try {
            const module = await import(importPath);
            this.renderElement(element, module.default);
            this.retryCount.delete(importPath);
        } catch (error) {
            console.error(`Failed to load ${importPath}:`, error);
            
            if (retryCount < this.options.maxRetries) {
                this.retryCount.set(importPath, retryCount + 1);
                setTimeout(() => {
                    this.loadElement(element);
                }, this.options.retryDelay * (retryCount + 1));
            } else {
                this.renderFallback(element);
            }
        }
    }
    
    renderElement(element, module) {
        element.innerHTML = '';
        element.appendChild(module);
    }
    
    renderFallback(element) {
        if (this.options.fallbackComponent) {
            element.innerHTML = this.options.fallbackComponent;
        } else {
            element.innerHTML = '
Failed to load component
'; } } observe(element) { this.observer.observe(element); } disconnect() { this.observer.disconnect(); } } # 4. Lazy Loading Performance Monitor class LazyLoadMonitor { constructor() { this.metrics = { totalElements: 0, loadedElements: 0, failedElements: 0, loadTimes: [], errors: [] }; this.startTime = performance.now(); } recordLoad(element, loadTime, success = true) { this.metrics.totalElements++; if (success) { this.metrics.loadedElements++; this.metrics.loadTimes.push(loadTime); } else { this.metrics.failedElements++; } } recordError(error) { this.metrics.errors.push({ message: error.message, timestamp: Date.now() }); } getStats() { const totalTime = performance.now() - this.startTime; const averageLoadTime = this.metrics.loadTimes.length > 0 ? this.metrics.loadTimes.reduce((sum, time) => sum + time, 0) / this.metrics.loadTimes.length : 0; return { ...this.metrics, totalTime, averageLoadTime, successRate: (this.metrics.loadedElements / this.metrics.totalElements) * 100, errorRate: (this.metrics.failedElements / this.metrics.totalElements) * 100 }; } generateReport() { const stats = this.getStats(); console.log('=== Lazy Loading Performance Report ==='); console.log(`Total Elements: ${stats.totalElements}`); console.log(`Loaded Elements: ${stats.loadedElements}`); console.log(`Failed Elements: ${stats.failedElements}`); console.log(`Success Rate: ${stats.successRate.toFixed(2)}%`); console.log(`Average Load Time: ${stats.averageLoadTime.toFixed(2)}ms`); console.log(`Total Time: ${stats.totalTime.toFixed(2)}ms`); } } # 5. Lazy Loading with Priority class PriorityLazyLoader { constructor() { this.priorities = new Map(); this.observer = new IntersectionObserver( this.handleIntersection.bind(this), { threshold: 0.1, rootMargin: '50px' } ); this.loadingQueue = []; this.isLoading = false; } setPriority(element, priority) { this.priorities.set(element, priority); } handleIntersection(entries) { entries.forEach(entry => { if (entry.isIntersecting) { const priority = this.priorities.get(entry.target) || 0; this.loadingQueue.push({ element: entry.target, priority }); this.loadingQueue.sort((a, b) => b.priority - a.priority); this.processQueue(); this.observer.unobserve(entry.target); } }); } async processQueue() { if (this.isLoading || this.loadingQueue.length === 0) return; this.isLoading = true; while (this.loadingQueue.length > 0) { const { element } = this.loadingQueue.shift(); await this.loadElement(element); } this.isLoading = false; } async loadElement(element) { const importPath = element.dataset.lazyLoad; if (!importPath) return; try { const module = await import(importPath); element.innerHTML = ''; element.appendChild(module.default); } catch (error) { console.error('Failed to load element:', error); } } observe(element) { this.observer.observe(element); } disconnect() { this.observer.disconnect(); } }

Best Practices

Lazy Loading Guidelines

Lazy Loading Best Practices

  • Use native loading="lazy" for images
  • Implement intersection observer
  • Add loading states and fallbacks
  • Preload critical content
  • Monitor performance metrics
  • Handle errors gracefully
  • Use appropriate thresholds

Common Mistakes

  • Lazy loading everything
  • No loading states
  • Poor error handling
  • Incorrect thresholds
  • No performance monitoring
  • Ignoring accessibility
  • Over-optimization

Summary

Lazy loading implementation involves several key components:

Need More Help?

Struggling with lazy loading implementation or need help optimizing your website's loading performance? Our performance experts can help you implement effective lazy loading strategies.

Get Lazy Loading Help