Code Splitting vs Lazy Loading in React and Next.js — A Beginner-Friendly Deep Dive

This blog explains the difference between code splitting and lazy loading in React and Next.js. It walks beginners through how these two techniques work together to improve app performance, reduce bundle size, and optimize user experience. With clear examples and real-world use cases, readers will understand when and how to apply lazy loading effectively — especially in Next.js App Router projects.
Have you ever noticed how some websites load instantly while others take forever — even though they look similar?
That’s often the difference between loading everything at once and loading only what’s needed.
Two powerful techniques make this possible in React and Next.js: 👉 Code Splitting and Lazy Loading.
They sound similar, but they’re not the same. In this guide, we’ll explore both — what they mean, why they matter, and how to use them to build faster, smoother apps.
🚀 Why You Should Care About These Techniques
When a web app loads, the browser has to:
- Download all the JavaScript.
- Parse and execute it before showing the page.
If your app is small, that’s fine. But once you add charts, maps, rich editors, or analytics tools, your JavaScript bundle gets huge — slowing everything down.
That’s where code splitting and lazy loading come to the rescue:
- They break big code into smaller chunks, and
- Load those chunks only when needed.
Result? ✅ Faster page loads ✅ Lower memory use ✅ Happier users (and search engines)
🧩 What Is Code Splitting?
Code Splitting means dividing your JavaScript bundle into smaller pieces (chunks) instead of shipping one giant file to the browser.
It’s like splitting a thick textbook into separate chapters — the browser only loads the chapter (page) the user opens.
Example: How Next.js Does Code Splitting Automatically
In Next.js, each page inside the /app or /pages folder becomes its own bundle automatically:
jsx// app/page.tsx export default function Home() { return <h1>Home</h1>; } // app/dashboard/page.tsx export default function Dashboard() { return <h1>Dashboard</h1>; }
When you visit /, Next.js only sends the Home page code.
When you go to /dashboard, it loads that page’s bundle separately.
💡 Why It Matters
- Faster initial load time
- Better Core Web Vitals
- Reduced data usage on slow networks
🧠 Think of code splitting as something that happens at build time — it’s your app’s structure being divided into smaller files before deployment.
💤 What Is Lazy Loading?
Lazy Loading controls when those code chunks are fetched.
Instead of loading everything immediately, the browser waits until a part of your app is actually needed — then it downloads that part on demand.
For example, a chart on your dashboard might not need to load until the user scrolls down to see it.
Example: Lazy Loading in React
jsximport React, { Suspense } from "react"; const Chart = React.lazy(() => import("./Chart")); function Dashboard() { return ( <Suspense fallback={<p>Loading chart...</p>}> <Chart /> </Suspense> ); }
Here’s what happens:
- React splits the
Chartcomponent into its own chunk. - That chunk is only loaded when
<Chart />is rendered. - The fallback (
Loading chart...) shows temporarily while it loads.
So, lazy loading happens at runtime — when the app is already running in the browser.
⚙️ Lazy Loading in Next.js (App Router Style)
Next.js gives you an easy way to lazy load components using the next/dynamic function:
jsximport dynamic from "next/dynamic"; const Chart = dynamic(() => import("../components/Chart"), { loading: () => <p>Loading chart...</p>, ssr: false, }); export default function Dashboard() { return ( <> <Sidebar /> <Chart /> </> ); }
Explanation:
dynamic()turns your Chart into a lazy-loaded component.ssr: falsetells Next.js not to server-render it — useful for components that rely on browser APIs (like Chart.js or Maps).- The
loadingoption defines a simple placeholder or skeleton while the component loads.
💡 In Next.js 14+, pages in the App Router are Server Components by default, meaning only client-side interactivity (like charts or maps) should be lazy-loaded.
🧠 Code Splitting vs Lazy Loading — The Difference
| Feature | Code Splitting | Lazy Loading |
|---|---|---|
| What it does | Divides code into smaller chunks | Loads those chunks on demand |
| When it happens | At build time | At runtime |
| Who controls it | Next.js / Webpack automatically | You decide when to load |
| Goal | Reduce bundle size | Improve perceived speed |
| Example | Each route gets its own JS bundle | Chart loads only when rendered |
👉 Code splitting creates smaller bundles. 👉 Lazy loading decides when to load them.
Together, they make your app faster and lighter.
⚡️ Real-World Use Cases
Lazy loading is best when a component is heavy, optional, or not immediately visible.
✅ Use Lazy Loading For:
- Charts (Chart.js, Recharts)
- Maps (Google Maps, Leaflet)
- Text editors (React Quill, Monaco)
- Modals or popups
- Image galleries or carousels below the fold
🚫 Avoid Lazy Loading For:
- Navbar
- Hero section
- Main content
- SEO-critical or above-the-fold UI
If you lazy-load your main UI, users will just stare at a blank screen longer.
🧩 Summary Rule of Thumb
| Situation | Lazy Load? | Why |
|---|---|---|
| Navbar, Hero, Main Content | ❌ | Needed immediately |
| Charts, Maps, Code Editors | ✅ | Heavy and secondary |
| Modal opened later | ✅ | Only needed on user action |
| Common Buttons, Layout | ❌ | Too small; reused everywhere |
| Image gallery below the fold | ✅ | Improves initial load speed |
⚖️ A Balanced Example — Dashboard Page
Here’s a smart way to structure your dashboard in Next.js 14+:
app/dashboard/page.tsx
jsximport Sidebar from "@/components/Sidebar"; import Chart from "@/components/Chart.lazy"; import Editor from "@/components/Editor.lazy"; export default function DashboardPage() { return ( <> <Sidebar /> <Chart /> <Editor /> </> ); }
components/Chart.lazy.tsx
jsx"use client"; import dynamic from "next/dynamic"; const Chart = dynamic(() => import("./Chart"), { loading: () => <p>Loading chart...</p>, ssr: false, }); export default Chart;
✅ Server Component (page) stays light and SEO-friendly. ✅ Chart and Editor load only on the client. ✅ Clean separation and faster overall experience.
🧮 Measuring the Impact
1. Analyze Bundle Size with @next/bundle-analyzer
Install:
bashnpm install @next/bundle-analyzer
In next.config.js:
jsconst withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }); module.exports = withBundleAnalyzer({});
Then run:
bashANALYZE=true npm run build
You’ll see a visual report showing which bundles are large.
After lazy loading, those heavy components (like Chart.js) move into separate async chunks.
2. Test Performance with Lighthouse
Run a Lighthouse test (Chrome DevTools → “Lighthouse” tab).
You’ll likely see major improvements:
| Metric | Before | After Lazy Loading |
|---|---|---|
| First Contentful Paint (FCP) | ~2.5s | ~1.2s |
| Largest Contentful Paint (LCP) | ~3.0s | ~1.6s |
| Total Blocking Time (TBT) | ~500ms | ~150ms |
That’s a noticeable boost in real-world performance.
🧭 Best Practices
| Rule | Recommendation |
|---|---|
| Split routes automatically | Let Next.js handle page-level code splitting |
| Lazy-load heavy UI | Charts, Maps, Editors, Modals |
| Keep critical UI eager | Navbar, Hero, Core Content |
| Use friendly fallbacks | Skeletons or loading placeholders |
| Check bundle size | Use @next/bundle-analyzer |
| Measure results | Lighthouse, WebPageTest, or Chrome DevTools |
✨ Final Thoughts
Code Splitting and Lazy Loading are like teammates:
- One cuts your code into pieces,
- The other decides when to load each piece.
In Next.js (especially versions 14 and 15), both are built-in and incredibly easy to use.
Use them wisely — lazy load what’s secondary, and render what’s essential. Your users (and Google’s performance metrics) will thank you for it.