React Performance Case Study: Code Splitting, Lazy Loading, and Data Caching in Production
A practical React performance optimization case study covering code splitting, lazy loading, caching, Core Web Vitals, and faster page loads.
React Performance Case Study: Code Splitting, Lazy Loading, and Data Caching in Production
React performance work gets real when users are waiting on actual pages, not demo counters. In production projects like Reflys and Procurement League, I worked on reducing load time, improving data responsiveness, and making complex interfaces feel lighter without removing the features users needed.
The most effective improvements were not exotic. They came from measuring the right bottlenecks and applying a disciplined mix of code splitting, lazy loading, data caching, and rendering cleanup.
This is the practical React performance optimization process I use when a product needs to get faster.
Start with the User Journey
Before touching code, identify the journeys that matter:
- First page load from a marketing link.
- Dashboard load after authentication.
- Search and filter interactions.
- Detail pages with API-heavy data.
- Mobile users on slower connections.
Performance is not one score. A dashboard with a fast first paint but a slow filter interaction still feels broken. A landing page with a beautiful animation but heavy JavaScript can lose users before they see the message.
Step 1: Measure the Real Bottleneck
I usually split performance issues into four buckets:
| Bottleneck | Symptom | Common Fix |
|---|---|---|
| JavaScript bundle | Slow first load, long blocking time | Code splitting, dependency cleanup |
| Data fetching | Skeletons stay visible too long | Caching, pagination, query tuning |
| Rendering | UI stutters during interactions | Memoization, state isolation |
| Assets | Images or media delay LCP | Image sizing, lazy loading, CDN |
For user-facing sites, Core Web Vitals matter. For dashboards, interaction speed and data freshness matter just as much.
Step 2: Split by Route and Feature
React applications often get slow because every page pays for features it does not use. Route-level splitting is the baseline. Feature-level splitting is where larger wins usually appear.
import dynamic from "next/dynamic";
const AnalyticsPanel = dynamic(() => import("./AnalyticsPanel"), {
loading: () => <PanelSkeleton />,
});
const CampaignEditor = dynamic(() => import("./CampaignEditor"), {
ssr: false,
loading: () => <EditorSkeleton />,
});
Use dynamic imports for:
- Heavy charts.
- Rich text editors.
- Map components.
- Admin-only modules.
- Modals that are not visible on first paint.
The goal is not to split everything. The goal is to stop shipping expensive code before the user needs it.
Step 3: Lazy Load Visual Weight
Images can quietly destroy performance. On product pages and portfolio-style interfaces, the solution is usually a combination of correct dimensions, responsive sizes, and lazy loading.
import Image from "next/image";
export function ProjectPreview() {
return (
<Image
src="/projects/reflys/one.png"
alt="Reflys dashboard preview"
width={1200}
height={800}
sizes="(max-width: 768px) 100vw, 50vw"
priority={false}
loading="lazy"
/>
);
}
Only prioritize the image that directly affects Largest Contentful Paint. Everything else should earn its place in the initial payload.
Step 4: Cache Data Intentionally
Procurement and commerce interfaces often have data-heavy screens: filters, reports, tables, search, and detail views. If every interaction triggers a cold request, users feel the delay instantly.
For GraphQL apps, Apollo Client can reduce repeated network requests with a predictable cache policy.
const { data, loading } = useQuery(GET_PROCUREMENT_ITEMS, {
variables: { filters, page },
fetchPolicy: "cache-and-network",
nextFetchPolicy: "cache-first",
});
For REST-heavy React and Next.js apps, TanStack Query is a strong default:
const projectsQuery = useQuery({
queryKey: ["projects", filters],
queryFn: () => getProjects(filters),
staleTime: 60_000,
gcTime: 5 * 60_000,
});
The key decision is freshness. Not every page needs real-time data. Many dashboards can be fast and accurate with a short stale window plus manual refresh.
Step 5: Keep State Close to the UI
Global state is useful, but it can become a rendering tax. If a small filter change rerenders the entire page, the app will feel heavy even if the API is fast.
Good state placement:
- URL state for shareable filters.
- Server state in TanStack Query or Apollo.
- Local component state for open menus, tabs, and form drafts.
- Global state only for cross-cutting concerns like auth, theme, or shared workflow state.
When I optimize a React app, I look for state that escaped too far upward. Pulling it back down often removes unnecessary renders without adding complexity.
Step 6: Memoize After You Find the Hot Path
useMemo, useCallback, and React.memo are helpful, but they are not a substitute for good architecture. I use them when profiling shows a repeated expensive calculation or a child component receives stable props.
const filteredItems = useMemo(() => {
return items.filter((item) => {
return item.status === selectedStatus && item.name.includes(search);
});
}, [items, selectedStatus, search]);
Memoization should make the code more predictable, not more fragile. If dependency arrays become hard to reason about, the component may need to be split.
Step 7: Protect the Main Thread
Animation-heavy pages can look premium and still feel slow if motion competes with rendering. For interfaces using Framer Motion, I keep animations focused:
- Animate transform and opacity when possible.
- Avoid layout-heavy animations on large lists.
- Reduce motion for users who request it.
- Do not animate every item at once on mobile.
- Lazy load animation-heavy sections below the fold.
Performance is part of design quality. Smooth motion should support the product, not tax it.
The 30% Load-Time Improvement Pattern
In Reflys, the performance win came from a familiar stack of improvements:
- Split heavy interactive sections from the initial bundle.
- Lazy loaded non-critical visuals.
- Reduced repeated API fetches with cleaner state synchronization.
- Kept page transitions smooth without blocking the first render.
- Reviewed dependencies and removed unnecessary client-side weight.
None of those changes were flashy on their own. Together, they helped reduce page load times by 30%.
Production Checklist
Use this checklist before shipping a React performance pass:
- Run Lighthouse on mobile and desktop.
- Inspect bundle size by route.
- Confirm the LCP element is optimized.
- Lazy load below-the-fold media.
- Split heavy components behind user intent.
- Cache server data with clear stale rules.
- Profile slow interactions in React DevTools.
- Test on a slower network profile.
- Watch real user metrics after deployment.
Performance optimization is not finished when the build passes. It is finished when the user journey feels faster in production.
Related Reading
Final Thoughts
The best React performance work is boring in the right way: measure, remove waste, split expensive work, cache responsibly, and keep rendering predictable.
That is how real products get faster without becoming harder to maintain.