šŸ’¤
Intermediate
10 min read

Lazy 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:


Description


Description


Description

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 webpackPrefetch for resources needed soon
  • Preload critical resources with <link rel="preload">
  • Monitor chunk sizes - keep initial bundle under 200KB
  • Use font-display: swap to prevent FOIT (Flash of Invisible Text)
  • Implement skeleton screens for better perceived performance
  • Test on slow networks to ensure smooth loading experience