Skip to content
Performance

Lazy Loading Patterns

Implement lazy loading for images, components, routes, and data to speed up initial page loads.

stellae.design

7 min read

Introduction

Lazy loading defers the loading of non-critical resources until they're actually needed. Instead of downloading everything upfront, you load content as users scroll or interact. This dramatically reduces initial page weight.

Modern browsers support native lazy loading for images and iframes, but the pattern extends to components, routes, and entire features.

This guide covers patterns from simple image deferral to advanced code-splitting strategies.


Key Concepts

Native Lazy Loading

html
<img src="photo.webp" loading="lazy" alt="Photo" width="800" height="600">
<iframe src="https://youtube.com/embed/..." loading="lazy"></iframe>

Intersection Observer API

javascript
const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        entry.target.src = entry.target.dataset.src;
        observer.unobserve(entry.target);
      }
    });
  },
  { rootMargin: '200px' }
);
document.querySelectorAll('[data-src]').forEach(el => observer.observe(el));

Dynamic Imports

typescript
import { lazy, Suspense } from 'react';

const HeavyChart = lazy(() => import('./HeavyChart'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      {showChart && <HeavyChart data={data} />}
    </Suspense>
  );
}

Practical Examples

1. Route-Based Code Splitting

typescript
import dynamic from 'next/dynamic';

const AdminDashboard = dynamic(() => import('@/components/AdminDashboard'), {
  loading: () => <DashboardSkeleton />,
  ssr: false,
});

2. Below-Fold Component Loading

typescript
function LazySection({ children }) {
  const [visible, setVisible] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    const obs = new IntersectionObserver(
      ([e]) => { if (e.isIntersecting) { setVisible(true); obs.disconnect(); } },
      { rootMargin: '100px' }
    );
    if (ref.current) obs.observe(ref.current);
    return () => obs.disconnect();
  }, []);

  return <div ref={ref}>{visible ? children : <Skeleton />}</div>;
}

3. Lazy Loading Third-Party Scripts

javascript
let loaded = false;
function loadAnalytics() {
  if (loaded) return;
  loaded = true;
  const s = document.createElement('script');
  s.src = 'https://analytics.example.com/script.js';
  document.head.appendChild(s);
}
['click','scroll','keydown'].forEach(e =>
  document.addEventListener(e, loadAnalytics, { once: true })
);

Best Practices

  • ✅ Use native loading='lazy' for images and iframes below the fold
  • ✅ Never lazy-load the LCP element
  • ✅ Provide meaningful loading states (skeletons, not spinners)
  • ✅ Set rootMargin on IntersectionObserver to preload before visible
  • ✅ Code-split routes so users only download code for the current page
  • ❌ Don't lazy-load everything — above-fold content should load immediately

Common Pitfalls

  • 🚫 Lazy loading the hero image — directly hurts LCP
  • 🚫 No loading state — users see blank space
  • 🚫 Too many dynamic imports — each creates a network request
  • 🚫 Not testing on slow connections

Performanceperformance

Was this article helpful?