š¤
Intermediate
10 min readLazy Loading & Code Splitting
Loading resources on demand to improve initial load time
Understanding Lazy Loading
Lazy loading is a strategy to defer loading of non-critical resources until they're needed. This significantly improves initial page load time and reduces bandwidth usage.
Image Lazy Loading
Modern browsers support native lazy loading for images:

Intersection Observer API
For more control and broader browser support, use Intersection Observer:
// Custom lazy loading with Intersection Observer
class LazyLoader {
constructor(options = {}) {
this.options = {
root: null,
rootMargin: '50px',
threshold: 0.01,
...options
};
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
this.options
);
}
observe(element) {
this.observer.observe(element);
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
// Load image
if (img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
}
// Load srcset
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
img.removeAttribute('data-srcset');
}
// Stop observing
this.observer.unobserve(img);
}
});
}
}
// Usage
const lazyLoader = new LazyLoader({ rootMargin: '100px' });
document.querySelectorAll('img[data-src]').forEach(img => {
lazyLoader.observe(img);
});
Code Splitting with Dynamic Imports
// ā Bad: Import everything upfront
import { HeavyChart } from './heavy-chart';
import { VideoPlayer } from './video-player';
import { ImageEditor } from './image-editor';
// All modules loaded immediately, even if never used
// ā
Good: Dynamic imports - load on demand
async function showChart(data) {
const { HeavyChart } = await import('./heavy-chart');
const chart = new HeavyChart(data);
chart.render();
}
async function playVideo(url) {
const { VideoPlayer } = await import('./video-player');
const player = new VideoPlayer(url);
player.play();
}
// ā
Good: With error handling and loading state
async function loadModule(modulePath) {
try {
showLoader();
const module = await import(modulePath);
hideLoader();
return module;
} catch (error) {
console.error('Failed to load module:', error);
showError('Failed to load component');
}
}
React Lazy Loading
import React, { lazy, Suspense } from 'react';
// ā Bad: Import all components upfront
import Dashboard from './Dashboard';
import Settings from './Settings';
import Profile from './Profile';
// ā
Good: Lazy load route components
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
const Profile = lazy(() => import('./Profile'));
function App() {
return (
}>
} />
} />
} />
);
}
// ā
Good: Lazy load on user interaction
function ImageEditor() {
const [EditorComponent, setEditorComponent] = useState(null);
const loadEditor = async () => {
const { ImageEditor } = await import('./heavy-editor');
setEditorComponent(() => ImageEditor);
};
return (
{!EditorComponent ? (
) : (
}>
)}
);
}
Webpack Code Splitting
// Magic comments for webpack
// Prefetch: Load during idle time
import(/* webpackPrefetch: true */ './components/Modal');
// Preload: Load in parallel with parent
import(/* webpackPreload: true */ './components/Header');
// Custom chunk name
import(/* webpackChunkName: "admin-panel" */ './admin/Panel');
// Combine multiple options
import(
/* webpackChunkName: "charts" */
/* webpackPrefetch: true */
'./components/Chart'
);
// Conditional loading
if (user.isAdmin) {
import(/* webpackChunkName: "admin" */ './admin').then(admin => {
admin.initializeAdminPanel();
});
}
Route-Based Code Splitting
// Next.js automatic code splitting
// Each page is automatically split into its own bundle
// pages/index.js - Main bundle
export default function Home() {
return Home
;
}
// pages/about.js - Separate bundle (only loaded when navigating to /about)
export default function About() {
return About
;
}
// Dynamic imports in Next.js
import dynamic from 'next/dynamic';
// Without SSR
const DynamicComponent = dynamic(() => import('../components/heavy'), {
ssr: false,
loading: () => Loading...
});
// With custom loading
const DynamicChart = dynamic(
() => import('../components/Chart'),
{
loading: () => ,
ssr: true
}
);
Font Loading Optimization
/* ā
Good: Font loading strategies */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap; /* Show fallback immediately, swap when loaded */
/* Other options: auto, block, fallback, optional */
}
/* Load fonts based on content visibility */
.hero-text {
font-family: 'MyFont', sans-serif;
}
/* Preload critical fonts */
Measuring Impact
// Measure dynamic import performance
async function measureImport(modulePath) {
const startTime = performance.now();
try {
const module = await import(modulePath);
const loadTime = performance.now() - startTime;
console.log(`Module loaded in ${loadTime}ms`);
// Send to analytics
sendMetric('module_load_time', loadTime, { module: modulePath });
return module;
} catch (error) {
console.error('Import failed:', error);
throw error;
}
}
// Monitor bundle sizes
if (process.env.NODE_ENV === 'development') {
import('webpack-bundle-analyzer').then(({ BundleAnalyzerPlugin }) => {
// Analyze bundle composition
});
}
Best Practices
- Use native
loading="lazy"for images below the fold - Implement Intersection Observer for custom lazy loading logic
- Split code at route boundaries for single-page applications
- Lazy load expensive components only when needed
- Use
webpackPrefetchfor resources needed soon - Preload critical resources with
<link rel="preload"> - Monitor chunk sizes - keep initial bundle under 200KB
- Use
font-display: swapto prevent FOIT (Flash of Invisible Text) - Implement skeleton screens for better perceived performance
- Test on slow networks to ensure smooth loading experience