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
# 2. Lazy Loading with Placeholder
# 3. Responsive Images with Lazy Loading
# 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... }>
{showChart && (
Loading chart...
}>
Loading...
}) {
const [ref, isIntersecting, hasIntersected] = useIntersectionObserver();
return (
{hasIntersected ? (
{children}
) : (
);
}
# Usage
function App() {
return (
Scroll to load component
)}
My App
{loading && }
);
}
# 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 (
Loading chart...
}
{error && Error loading chart: {error.message}
}
{Chart && Failed to load component
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
Loading chart...
# 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
Loading...
Error: {{ error.message }}
# 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:
- Native Lazy Loading: HTML loading attribute, intersection observer
- Component Lazy Loading: React.lazy, Vue async components
- Advanced Techniques: Preloading, caching, error recovery
- Performance Monitoring: Metrics, reporting, optimization
- Best Practices: Guidelines, common pitfalls, accessibility
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
# 3. Responsive Images with Lazy Loading