Code Splitting and Lazy Loading with Claude Code
Use Claude Code to implement React and Next.js code splitting safely, with examples, pitfalls, checks, and CTA guidance.
When a React or Next.js app starts feeling slow, the first suspect is often the JavaScript bundle. A visitor may only need the landing page, but the browser still downloads admin screens, charts, editors, media players, and settings panels.
Code splitting means breaking that JavaScript into smaller chunks. Lazy loading means downloading a chunk only when the user actually needs it. In plain terms, you stop shipping the whole toolbox to every page view.
This guide shows how to ask Claude Code for the right change, not just any change. We will use React.lazy, Suspense, dynamic import(), Next.js dynamic, route/page-level splitting, hydration checks, and rollout verification. For the wider performance workflow, connect this article with bundle analysis, tree shaking, and performance optimization.
Key Terms Before Editing
bundle is the JavaScript package sent to the browser. chunk is a smaller file created after splitting. dynamic import is the import("./Chart") syntax that loads a module at runtime. Suspense is the React boundary that shows a fallback while a lazy component is loading.
hydration is the step where browser JavaScript attaches events and state to HTML that was already rendered. If server output and client output do not match, you can get hydration errors. Lazy loading often exposes those errors when code reads window, localStorage, the current time, or random values too early.
Good candidates for lazy loading are heavy, optional, and not needed for the first meaningful screen.
| Candidate | Why it fits | Watch out for |
|---|---|---|
| Admin panels | Most visitors never open them | Keep auth and loading states clear |
| Charts, maps, editors | Heavy dependencies | Reserve space to avoid layout shift |
| Modals and wizards | Needed after a click | Prefetch if the first click feels slow |
| Media/search widgets | Often below the fold | Keep keyboard and screen reader behavior intact |
Prompt Claude Code with Boundaries
Do not ask only for “code splitting.” Give Claude Code the target files, protected UI, and verification steps.
Goal:
- Lazy-load heavy UI that is not needed on first paint.
- Do not move the hero, article body, navigation, or CTA behind lazy loading.
- Use React.lazy/Suspense or next/dynamic depending on the framework.
Targets:
- src/features/reports/ReportsPanel.tsx
- src/features/editor/RichEditor.tsx
- app/admin/page.tsx
Verification:
- npm run lint
- npm run build
- Check the Network tab for initial JS and lazy chunks.
- Check mobile layout so the fallback does not cover the CTA.
This keeps the task small enough to review. It also tells Claude Code that revenue paths and first-screen content are not optional collateral.
React.lazy and Suspense
React’s official docs describe lazy as a way to defer loading a component until it is rendered for the first time, with Suspense displaying a fallback while the code loads. Declare lazy components at module top level, not inside another component, so state is not reset on every render.
// src/App.tsx
import { Suspense, lazy, useState } from "react";
const ReportsPanel = lazy(() => import("./ReportsPanel"));
function PanelSkeleton() {
return (
<div role="status" aria-live="polite" style={{ minHeight: 180 }}>
Loading reports...
</div>
);
}
export default function App() {
const [showReports, setShowReports] = useState(false);
return (
<main>
<h1>Dashboard</h1>
<button type="button" onClick={() => setShowReports(true)}>
Show reports
</button>
{showReports ? (
<Suspense fallback={<PanelSkeleton />}>
<ReportsPanel />
</Suspense>
) : (
<p>The heavy reporting UI loads only when it is needed.</p>
)}
</main>
);
}
// src/ReportsPanel.tsx
const rows = [
{ label: "Article completion", value: "68%" },
{ label: "CTA clicks", value: "4.2%" },
{ label: "Consultation visits", value: "1.1%" },
];
export default function ReportsPanel() {
return (
<section aria-label="Reports">
<h2>Conversion report</h2>
<ul>
{rows.map((row) => (
<li key={row.label}>
{row.label}: {row.value}
</li>
))}
</ul>
</section>
);
}
lazy expects a default export. For named exports, wrap the module result.
// src/lazyNamed.tsx
import { lazy, type ComponentType } from "react";
export function lazyNamed<TModule, TName extends keyof TModule>(
loader: () => Promise<TModule>,
name: TName
) {
return lazy(async () => {
const module = await loader();
return {
default: module[name] as ComponentType,
};
});
}
// src/AnalyticsSlot.tsx
import { Suspense } from "react";
import { lazyNamed } from "./lazyNamed";
const BarChart = lazyNamed(() => import("./charts"), "BarChart");
export function AnalyticsSlot() {
return (
<Suspense fallback={<p>Loading chart...</p>}>
<BarChart />
</Suspense>
);
}
Next.js dynamic Import
Next.js already splits code around pages and routes. Use next/dynamic when a specific Client Component is heavy or depends on browser-only APIs. Keep the import() inside the dynamic() call and declare it at module top level so the framework can map and preload the chunk.
// app/admin/EditorSlot.tsx
"use client";
import dynamic from "next/dynamic";
const RichEditor = dynamic(() => import("./RichEditor"), {
ssr: false,
loading: () => (
<p aria-live="polite" style={{ minHeight: 160 }}>
Loading editor...
</p>
),
});
export default function EditorSlot() {
return <RichEditor initialMarkdown="# Draft" />;
}
// app/admin/page.tsx
import EditorSlot from "./EditorSlot";
export default function AdminPage() {
return (
<main>
<h1>Article editor</h1>
<p>The page copy and CTA render first. Only the heavy editor is delayed.</p>
<EditorSlot />
</main>
);
}
Use ssr: false carefully. It is useful for browser-only editors or previews, but it is a bad fit for article text, pricing, FAQs, or primary CTA content that should be present in server-rendered HTML.
Route and Page-Level Splitting
Start with route boundaries before splitting every component. In Next.js, separate app/reports/page.tsx, app/settings/page.tsx, and app/admin/page.tsx routes naturally keep page code apart. In a React Router SPA, lazy-load route components.
// src/AppRouter.tsx
import { Suspense, lazy } from "react";
import { BrowserRouter, Link, Route, Routes } from "react-router-dom";
const HomePage = lazy(() => import("./pages/HomePage"));
const ReportsPage = lazy(() => import("./pages/ReportsPage"));
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
function RouteFallback() {
return <p aria-live="polite">Loading page...</p>;
}
export default function AppRouter() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/reports">Reports</Link>
<Link to="/settings">Settings</Link>
</nav>
<Suspense fallback={<RouteFallback />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/reports" element={<ReportsPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Avoid putting the whole product page behind one large Suspense boundary. Keep the headline, body, and CTA visible, then wrap only the slow panel.
Practical Use Cases
First, SaaS dashboards. Load the basic shell and navigation immediately, then lazy-load admin-only analytics, audit logs, exports, or chart libraries after permission checks.
Second, editorial and course platforms. Article text, lesson titles, and purchase CTAs should render first. Markdown editors, image crop tools, preview panes, and internal review dashboards can be delayed.
Third, landing pages with maps, charts, video, or calculators. The visitor should see the promise and next step immediately. Heavy widgets can load after scroll or click. For media UI, pair this article with video player implementation and accessibility implementation.
Fourth, quote modals and checkout helpers. The button can be available immediately while the multi-step form loads on demand. If the first click feels slow, prefetch the chunk when the button enters the viewport.
Pitfalls to Catch in Review
Pitfall 1: splitting too aggressively. Many tiny chunks can increase request overhead and make the app feel slower after interaction.
Pitfall 2: declaring lazy or dynamic inside a component. That can reset state and prevent useful preloading. Keep declarations at module top level.
Pitfall 3: using a vague fallback. A fixed-height, accessible loading state prevents layout shift and tells the user what is happening.
Pitfall 4: hydration mismatch. Move browser-only reads into a Client Component or useEffect, and avoid rendering time/random-dependent text differently on server and client.
Pitfall 5: hiding SEO or monetization content behind ssr: false. Do not lazy-load the article title, product value proposition, pricing, FAQ, or primary CTA unless you have a specific reason and a measurement plan.
Verification Checklist
Claude Code review request:
- Confirm lazy/dynamic declarations are at module top level.
- Check default export versus named export handling.
- Check that Suspense boundaries are not too broad.
- Confirm ssr:false is not used for SEO-critical copy or CTAs.
- Search for window/date/random/localStorage hydration risks.
- Describe how to compare initial JS, lazy chunks, and request count.
Run the normal project checks:
npm run lint
npm run build
Then open DevTools, filter Network by JS, reload, and confirm heavy chunks are absent from the initial load. Trigger the report, editor, or modal and confirm its chunk appears only then. Test mobile width to ensure the fallback does not overlap the CTA.
Official Links and CTA
Use the official docs as the source of truth: React lazy, React Suspense, Next.js Lazy Loading, and Next.js Layouts and Pages.
If you want a repeatable Claude Code workflow, start with the free Claude Code cheatsheet. For reusable implementation and review prompts, use 50 Claude Code Prompt Templates. For team rollout, repository rules, and performance review habits, use Claude Code training and consultation.
Hands-On Note
Using this workflow, the reliable improvement came from deciding the protected content before editing: first-screen copy, navigation, and CTA stayed server-rendered, while report and editor code moved into lazy chunks. A vague “make it lazy” request produced broader Suspense boundaries and unnecessary ssr: false. Treat code splitting as product design plus verification, not just a syntax change.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Permission Safety Ladder: Expand Access Without Losing Control
A beginner-friendly ladder for moving Claude Code from read-only to limited edits, proof commands, and deploy checks.
Claude Code Small PR Proof Pack: Make Tiny Changes Reviewable
A practical proof pack for Claude Code PRs: diff, checks, public URL, CTA path, and rollback note.
Claude Code Review Gate Before Commit: Diff, Tests, Public URL, and CTA Checks
A commit-time review gate for Claude Code work: diff scope, build, public URL, revenue CTA links, missing tests, and unrelated files.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.