Modern React applications often struggle with performance when rendering large dashboards, infinite feeds, analytics panels, media-heavy pages, or long ecommerce catalogs. Rendering everything at once increases memory usage, slows down page interaction, and hurts Core Web Vitals.
A practical solution is lazy rendering. Instead of mounting all components immediately, you only render components when they become visible inside the viewport.
React developers commonly use lazy loading for images, but the same idea works for entire React components.
In this guide, you will learn how to create smart lazy rendering components in React using the Intersection Observer API.
Why Traditional Rendering Becomes a Problem
Imagine a dashboard page containing:
- 20 chart widgets
- Multiple API requests
- Heavy table components
- Video previews
- Interactive graphs
If React renders everything during the initial load:
- JavaScript execution time increases
- Main thread blocks longer
- Initial paint becomes slower
- Hydration time increases
- Mobile devices struggle badly
Google Lighthouse frequently flags these problems in large React applications.
Companies like Netflix, YouTube, and Amazon aggressively delay offscreen rendering to reduce startup costs.
What is Intersection Observer?
The Intersection Observer API is a browser API that detects when an element enters or exits the viewport.
Instead of continuously listening to scroll events, the browser efficiently handles visibility tracking internally.
This makes it far more performant than traditional scroll event listeners.
Typical use cases include:
- Infinite scrolling
- Lazy image loading
- Ad impression tracking
- Animation triggers
- Deferred component rendering
Basic Intersection Observer Example
Before integrating with React, understand the core API.
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log(“Element is visible”);
}
});
});const element = document.querySelector(“#target”);observer.observe(element);
Here:
- entries contain visibility information
- isIntersecting becomes true when visible
- observe() starts tracking an element
Creating a Smart Lazy Render Component in React
Now let’s build a reusable React component that renders children only when visible.
Step 1: Create the LazyRender Component
const containerRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{
threshold: 0.1,
}
);if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => observer.disconnect();
}, []);
return (
);
}
export default LazyRender;
How This Component Works
The component follows a simple rendering strategy:
- Create a placeholder container
- Observe viewport visibility
- Render children only when visible
- Disconnect observer after rendering
This prevents unnecessary component mounting during initial page load.
Using the LazyRender Component
import LazyRender from “./LazyRender”;
import HeavyChart from “./HeavyChart”;
function Dashboard() {
return (
Analytics Dashboard
);
}
import LazyRender from “./LazyRender”;
import HeavyChart from “./HeavyChart”;
function Dashboard() {
return (
Analytics Dashboard
);
}
The HeavyChart component now mounts only when users scroll near it.
Adding Loading Placeholders
Blank spaces create a poor user experience. Skeleton placeholders work much better.
function LazyRender({
children,
placeholder,
height = “200px”,
}) {
const containerRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{
threshold: 0.1,
}
);
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => observer.disconnect();
}, []);
return (
);
}
function LazyRender({
children,
placeholder,
height = “200px”,
}) {
const containerRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{
threshold: 0.1,
}
);
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => observer.disconnect();
}, []);
return (
);
}
Usage
<LazyRender
height=”400px”
placeholder={
}
>
Preloading Before Visibility
You can render slightly before the component becomes visible.
This creates smoother scrolling experiences.
{
rootMargin: “200px”;
}
Updated observer:
const observer = new IntersectionObserver(callback, {
threshold: 0.1,
rootMargin: “200px”,
});
This starts rendering 200px before the element enters the viewport.
Optimizing Large Lists
Lazy rendering becomes extremely valuable in:
- Social feeds
- Product listings
- Infinite scroll pages
- Video platforms
- Large documentation sites
Without lazy rendering, React may mount hundreds of components unnecessarily.
Even if hidden below the fold, React still performs:
- Virtual DOM creation
- Hook initialization
- State setup
- Event binding
- Memory allocation
Combining with React.lazy
You can combine viewport rendering with code splitting.
import React, { lazy, Suspense } from “react”;
const HeavyChart = lazy(() => import(“./HeavyChart”));
Then:
<LazyRender
placeholder={
}
>
}>
This creates two levels of optimization:
- Component mounts only when visible
- JavaScript bundle loads only when required
Preventing Observer Memory Leaks
One common mistake is forgetting to clean up.
Always disconnect observers inside cleanup functions.
Without cleanup:
- Observers remain active
- Memory usage increases
- Performance degrades over time
When Lazy Rendering Should NOT Be Used
Do not lazy render:
- Critical above-the-fold content
- Primary navigation
- SEO-critical server-rendered content
- Accessibility-critical elements
Search engines and users should immediately access important content.
SSR and Next.js Considerations
In Next.js applications, lazy rendering works best for client-side interactive sections.
For example:
- Analytics widgets
- Comments sections
- Recommendation engines
- Charts
- Media galleries
Be careful with SEO pages because delayed rendering may affect indexing if important content appears too late.
Real Performance Improvements
In production React applications, lazy rendering often improves:
- Largest Contentful Paint (LCP)
- Time to Interactive (TTI)
- Total Blocking Time (TBT)
- JavaScript execution time
A SaaS analytics dashboard with 35 widgets reduced initial render time from 4.8 seconds to 1.9 seconds after implementing viewport-based lazy rendering.
The biggest improvement came from preventing hidden chart libraries from mounting immediately.
Advanced Enhancement: Render Once Strategy
Sometimes components should remain mounted after visibility.
That is exactly what our implementation already does.
Once visible:
- State remains preserved
- Component stays mounted
- Scrolling back does not recreate it
This avoids unnecessary remounting costs.
Alternative Libraries
Popular libraries include:
- react-intersection-observer
- react-lazyload
- react-virtualized
- react-window
However, building your own lightweight component often gives better control and a smaller bundle size.
The Intersection Observer is one of the most practical browser APIs for improving React performance.
Instead of rendering everything immediately, smart lazy rendering lets your application focus only on what users actually see.
This approach becomes especially powerful in large dashboards, e-commerce platforms, analytics systems, and content-heavy applications.
When combined with code splitting, memoization, and virtualization, lazy rendering can dramatically improve user experience without adding heavy dependencies.



1 thought on “Create Smart Lazy Rendering Components in React with Intersection Observer”
test comments