Engineering
React Performance: The Optimization Playbook We Use on Every Project
Practical techniques that cut load times by 40-60% - no theory, just patterns we apply to real production React applications every week.
Key Takeaways
- Measure before optimizing - React DevTools Profiler and Lighthouse give you the data to prioritize, not guess
- Code splitting with React.lazy reduces initial bundle size by 30-50% on typical SaaS applications with 10+ routes
- Memoization (React.memo, useMemo, useCallback) should target expensive computations, not every component - over-memoizing adds complexity without measurable benefit
- Image optimization alone (WebP, lazy loading, responsive srcset) typically accounts for 40% of achievable performance gains
- Virtual scrolling for lists over 100 items eliminates the single most common performance bottleneck in data-heavy applications
Performance optimization in React isn't about applying every technique you've read about - it's about identifying the specific bottlenecks in your application and applying the right technique to each one. After optimizing 40+ production React applications, we've developed a systematic playbook that consistently delivers 40-60% improvements in key metrics. Here's the full approach.
Step 1: Measure everything before changing anything. The biggest performance mistake we see is premature optimization - developers adding React.memo to every component, wrapping every function in useCallback, and splitting every route into a lazy chunk, without first measuring whether any of these things are actually slow. Our process starts with three tools: Lighthouse (overall performance score, Core Web Vitals), React DevTools Profiler (component render times, re-render frequency), and Chrome DevTools Network tab (bundle sizes, loading waterfall). These three tools identify 90% of performance issues.
Step 2: Code splitting is your highest-leverage optimization. A typical SaaS application with 10+ routes ships a single JavaScript bundle of 400KB-1.2MB. Users on the login page download code for the dashboard, settings, admin panel, and every other route they haven't visited yet. Code splitting with React.lazy and Suspense solves this: each route loads only when the user navigates to it. We typically see initial bundle size drop by 30-50%, which directly translates to faster First Contentful Paint and Time to Interactive.
Step 3: Image optimization is almost always the biggest single win. Images account for 50-80% of page weight on most websites. Our standard image optimization checklist: convert to WebP format (30% smaller than JPEG at equivalent quality), implement responsive images with srcset (serve smaller images to mobile devices), lazy load all images below the fold (use loading='lazy' or Intersection Observer), set explicit width and height attributes (prevents layout shift), and use a CDN with automatic format negotiation. On a recent e-commerce project, image optimization alone reduced page load time from 4.2 seconds to 1.8 seconds - no React code changes required.
Step 4: Memoization - powerful but overused. React.memo, useMemo, and useCallback are tools for preventing unnecessary re-renders and recalculations. They're powerful when applied to expensive operations - a component that renders a complex SVG chart, a function that filters 10,000 items, a callback passed to a list of 500 child components. They're wasteful when applied to cheap operations - a simple text component, a function that formats a date, a callback passed to a single button. Every memoization adds memory overhead and complexity. Our rule: profile first, memoize the top 5 offenders, measure the improvement, and stop.
Step 5: Virtual scrolling for data-heavy interfaces. If your application renders lists of 100+ items - transaction histories, product catalogs, user directories, log viewers - virtual scrolling is mandatory. Libraries like react-window or react-virtuoso render only the visible items plus a small buffer, keeping DOM node count under 50 regardless of list length. We've seen applications go from 2-second scroll jank to 60fps smoothness by virtualizing a single list component.
Step 6: State management optimization. The most common React performance problem we encounter isn't slow rendering - it's unnecessary rendering caused by poor state management. A state update in a parent component triggers re-renders of all children, even if the state change is irrelevant to most of them. Solutions: move state as close to where it's used as possible (colocate state), split large context providers into focused ones (a theme context and a user context instead of one app context), and use state management libraries that support selective subscriptions (Zustand, Jotai) for global state that changes frequently.
Step 7: Bundle analysis and tree shaking. Install webpack-bundle-analyzer or vite-plugin-visualizer and look at what's actually in your bundle. We've found: full lodash imported instead of individual functions (400KB → 4KB), moment.js with all locales (300KB → 20KB with date-fns), unused component libraries still in the dependency tree, and development-only code shipped to production. Fixing these issues typically removes 100-300KB from the bundle with zero functionality change.
Step 8: Server-side rendering considerations. Not every React application needs SSR, but if SEO matters or your initial data fetch is expensive, SSR or static generation can dramatically improve Time to First Byte and First Contentful Paint. For our SaaS products (behind authentication), client-side rendering is fine - search engines don't need to index dashboard pages. For marketing sites, landing pages, and content-heavy applications, we use Next.js or Remix for SSR. The decision is straightforward: does Google need to see this page? If yes, SSR. If no, CSR.
The performance budget that keeps teams honest. We set performance budgets at the start of every project: Lighthouse performance score above 90, First Contentful Paint under 1.5 seconds, Time to Interactive under 3 seconds, total bundle size under 250KB gzipped, and no single route chunk over 50KB. These budgets are enforced in CI - a build that violates any budget fails the pipeline. This prevents the gradual performance degradation that happens when 'just one more library' gets added every sprint for 18 months.
Written by
Alina Petrova
Head of Design at Commit4Solutions Private
Newsletter
Engineering insights, delivered monthly
Join 2,400+ engineers and technical leaders who get our best articles on architecture decisions, team scaling, and production lessons - before they hit the blog.
No spam. Unsubscribe anytime. We respect your inbox.
