Component-Level Splitting — Lazy Loading Individual Components
In this tutorial, you will learn about Component. We cover key concepts, practical examples, and best practices to help you master this topic.
Component-level splitting lazy loads individual UI components within a page, deferring heavy elements like charts, editors, and image galleries.
What You'll Learn
By the end of this tutorial, you'll understand how to split at the component level within a single page, use visibility-based component loading, and implement conditional rendering with lazy imports.
Why It Matters
Even within a single page, not all components are equally important. A blog post with a chart at the bottom doesn't need chart library code on initial render. Component-level splitting defers heavy components until they're actually visible or needed.
Real-World Use
A product page has a hero image (eager), description (eager), specs table (eager), interactive 3D viewer (below fold, lazy), customer reviews (lazy), and a recommendation engine (lazy). Initial bundle is 80KB. Heavy components load on demand.
Component Splitting Flow
graph TD
A[Page loads] --> B[Render critical
components eagerly]
B --> C[Defer heavy
components]
C --> D{Component needed?}
D -->|Below fold| E[Intersection Observer
triggers load]
D -->|User action| F[Button click
triggers load]
D -->|Condition| G[State change
triggers load]
E --> H[Dynamic import
component chunk]
F --> H
G --> H
H --> I[Component renders]
style B fill:#27ae60,color:#fff
style C fill:#f39c12,color:#fff
style H fill:#4a90d9,color:#fff
style I fill:#27ae60,color:#fff
Conditional Component Loading
// ProductPage.jsx — Conditional component loading
import React, { useState, lazy, Suspense } from 'react';
// Eager: Critical components
import ProductHero from './ProductHero';
import ProductInfo from './ProductInfo';
import ProductSpecs from './ProductSpecs';
// Lazy: Non-critical components
const ProductReviews = lazy(() => import('./ProductReviews'));
const RelatedProducts = lazy(() => import('./RelatedProducts'));
const ImageGallery = lazy(() => import('./ImageGallery'));
const ComparisonTable = lazy(() => import('./ComparisonTable'));
function ProductPage({ product }) {
const [showGallery, setShowGallery] = useState(false);
const [showReviews, setShowReviews] = useState(false);
return (
<div className="product-page">
{/* Always render: Critical */}
<ProductHero product={product} />
<ProductInfo product={product} />
{/* Lazy: Loaded on user action */}
<button onClick={() => setShowGallery(true)}>
View Gallery
</button>
{showGallery && (
<Suspense fallback={<GallerySkeleton />}>
<ImageGallery images={product.images} />
</Suspense>
)}
<ProductSpecs product={product} />
{/* Lazy: Loaded on user action */}
<button onClick={() => setShowReviews(true)}>
Show Reviews ({product.reviewCount})
</button>
{showReviews && (
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={product.id} />
</Suspense>
)}
{/* Lazy: Dynamic import with IntersectionObserver */}
<LazySection>
<Suspense fallback={<RelatedSkeleton />}>
<RelatedProducts productId={product.id} />
</Suspense>
</LazySection>
<LazySection rootMargin="200px">
<Suspense fallback={<ComparisonSkeleton />}>
<ComparisonTable productId={product.id} />
</Suspense>
</LazySection>
</div>
);
}
Visibility-Based Component Loading
// components/LazySection.jsx — Load children when visible
import React, { useState, useEffect, useRef } from 'react';
export default function LazySection({ children, rootMargin = '100px', placeholder }) {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef(null);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.unobserve(element);
}
},
{ rootMargin }
);
observer.observe(element);
return () => observer.disconnect();
}, [rootMargin]);
return (
<div ref={ref} className="lazy-section">
{isVisible ? (
children
) : (
placeholder || <div className="section-placeholder" />
)}
</div>
);
}
// Usage
function BlogPage() {
return (
<article>
<h1>Blog Post</h1>
<p>Article content...</p>
{/* Chart loads only when scrolled into view */}
<LazySection rootMargin="200px">
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart data={chartData} />
</Suspense>
</LazySection>
{/* Comment section loads when visible */}
<LazySection rootMargin="100px">
<Suspense fallback={<CommentSkeleton />}>
<CommentSection postId="123" />
</Suspense>
</LazySection>
</article>
);
}
Heavy Component Patterns
// 1. Chart that loads only when scrolled to
function AnalyticsSection({ data }) {
return (
<LazySection rootMargin="300px">
<Suspense fallback={<ChartSkeleton />}>
<InteractiveChart data={data} />
</Suspense>
</LazySection>
);
}
// 2. Rich text editor that loads on click
function RichTextEditor({ initialContent, onSave }) {
const [editor, setEditor] = useState(null);
if (!editor) {
return (
<div className="editor-placeholder">
<p>Rich editor loads on demand</p>
<button onClick={() => setEditor(true)}>
Enable editing
</button>
</div>
);
}
return (
<Suspense fallback={<div>Loading editor...</div>}>
<EditorComponent
initialContent={initialContent}
onSave={onSave}
/>
</Suspense>
);
}
// 3. Image gallery with progressive loading
function ProgressiveGallery({ images }) {
const [loadedImages, setLoadedImages] = useState(new Set());
return (
<div className="gallery">
{images.map((img, index) => (
<div key={index} className="gallery-item">
{index < 2 ? (
// First 2 images load eagerly
<img src={img.src} alt={img.alt} />
) : (
// Remaining images lazy load
<LazyImage src={img.src} alt={img.alt} />
)}
</div>
))}
</div>
);
}
// 4. Map component that loads on interaction
function LocationMap({ coordinates }) {
const [showMap, setShowMap] = useState(false);
if (!showMap) {
return (
<div className="map-placeholder">
<img src="/static-map-thumbnail.jpg" alt="Map preview" />
<button onClick={() => setShowMap(true)}>
Load interactive map
</button>
</div>
);
}
return (
<Suspense fallback={<div>Loading map...</div>}>
<InteractiveMap coordinates={coordinates} />
</Suspense>
);
}
Chunk Optimization
// webpack.config.js — Component chunk optimization
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// Group all chart libraries together
charts: {
test: /[\\/]node_modules[\\/](chart\.js|d3|recharts)[\\/]/,
name: 'chart-vendors',
chunks: 'async',
priority: 20,
},
// Group editor libraries
editors: {
test: /[\\/]node_modules[\\/](draft-js|slate|codemirror)[\\/]/,
name: 'editor-vendors',
chunks: 'async',
priority: 20,
},
// Group map libraries
maps: {
test: /[\\/]node_modules[\\/](leaflet|mapbox-gl|@googlemaps)[\\/]/,
name: 'map-vendors',
chunks: 'async',
priority: 20,
},
},
},
},
};
Common Mistakes
- Lazy Loading every component. Navigation, header, footer, and sidebar are always visible. Don't lazy load them. Only lazy load components below the fold or on interaction.
- Not providing meaningful loading states. A generic spinner for every lazy component looks unprofessional. Show skeletons that match the component shape.
- Loading all lazy components at once when one becomes visible. If the user scrolls past a chart, reviews, and related products simultaneously, all three load at once. Prioritize and batch.
- Forgetting about shared state and contexts. Lazy loaded components lose React context if not wrapped properly. Ensure providers are above lazy boundaries.
- Not testing load order. Components should load in priority order (reviews before recommendations). Use Intersection Observer margins to sequence loading.
Practice Questions
- How does component-level splitting differ from route-level splitting?
- How do you load a component when it becomes visible in the viewport?
- What is a good placeholder for a lazy loaded chart component?
- How do you prioritize which lazy components load first?
- How do you handle shared dependencies between lazy components?
Challenge: Build a product page with 6 lazy-loaded sections: image gallery (on click), spec comparison table (on scroll), 3D viewer (below fold), reviews (below fold), recommendations (bottom), and FAQ (on click). Measure initial vs total loaded bundle size.
FAQ
Mini Project
Build a blog post page with component-level lazy loading: 4 lazy sections (interactive chart, image gallery, comment section, related posts), use Intersection Observer for auto-loading and click-to-load for the gallery, measure load impact, and implement priority-based loading order.
What's Next
You've mastered component-level splitting. Now learn about Webpack Chunks for fine-grained control over your chunk configuration.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro