Optimizing Asset Loading: Performance, Lazy Loading, Code Splitting
Master performance optimization in React. Learn how to use lazy loading, code splitting, and image optimization to keep your dashboard fast and efficient.
Previously in this course, we explored handling large datasets to keep our interfaces responsive. In this lesson, we shift our focus to the "hidden" side of performance: the assets our users download before they even see a single pixel of our dashboard.
If you aren't careful, your React application will grow into a massive JavaScript bundle that forces users to wait seconds before the first interaction. We will fix this by implementing code splitting, lazy loading, and image optimization.
The Cost of Everything at Once
When you build a dashboard, you likely have components for settings, profile management, and complex data visualizations. If you import all of these at the top of your App.js, your browser must download, parse, and execute the entire codebase before the user can see even the login screen.
This is the primary driver of poor "Time to Interactive" (TTI) metrics. We need to split our code so that the user only downloads what they need for the current view.
Implementing Code Splitting with React.lazy
React provides a built-in way to defer the loading of components until they are actually rendered. This is called lazy loading.
Instead of a static import, we use React.lazy() to create a dynamic import. When the component is first rendered, React will trigger a network request to fetch the separate JavaScript file.
Worked Example: Lazy Loading Routes
In our dashboard project, let's lazy load the heavy Reports and Settings pages.
JAVASCRIPTimport React, { Suspense, lazy } from CE9178">'react'; import { BrowserRouter as Router, Routes, Route } from CE9178">'react-router-dom'; // Instead of static imports: // import Reports from CE9178">'./pages/Reports'; // import Settings from CE9178">'./pages/Settings'; // Use lazy loading: const Reports = lazy(() => import(CE9178">'./pages/Reports')); const Settings = lazy(() => import(CE9178">'./pages/Settings')); function App() { return ( <Router> {/* Suspense is required to show a fallback while the chunk loads */} <Suspense fallback={<div>Loading page...</div>}> <Routes> <Route path="/reports" element={<Reports />} /> <Route path="/settings" element={<Settings />} /> </Routes> </Suspense> </Router> ); }
By wrapping your routes in Suspense, you provide a "loading state" while the browser fetches the separate chunk. This significantly reduces the initial bundle size of your main main.js file.
Image Optimization Strategies
While JavaScript bloat is a major issue, raw, unoptimized images are often the silent killers of web performance. A 5MB dashboard background image will ruin your Core Web Vitals regardless of how well your code is split.
- Use Modern Formats: Always prefer WebP or AVIF over PNG or JPEG. They provide superior compression with no loss in visual quality.
- Responsive Images: Use the
srcsetattribute or a library to serve smaller images to mobile devices and high-resolution images to desktops. - Lazy Load Images: Just as we lazy load components, you should lazy load images that are "below the fold." Use the native
loading="lazy"attribute on<img>tags.
For a deeper dive into managing assets, see Handling Media in React: Optimizing Images and Loading States.
Hands-on Exercise: Audit and Split
- Open your dashboard project and run
npm run build. Look at the console output to identify your largest JavaScript chunks. - Identify a page component that is rarely visited immediately upon login (e.g., "Settings" or "Profile").
- Refactor that component to use
React.lazy. - Add a
Suspenseboundary with a loading spinner to ensure the UI doesn't "flicker" while the chunk fetches.
Common Pitfalls
- Over-splitting: Don't lazy load every single small component. The overhead of making multiple network requests can sometimes be worse than downloading one slightly larger file. Split at the route level first.
- Missing Suspense: If you forget to wrap a lazy component in
Suspense, React will throw an error. - Layout Shift: If your
fallbackUI is a different size than the loaded component, you will trigger Layout Shift, which hurts your SEO and user experience. Ensure your loading skeletons match the dimensions of the final content.
Recap
We've improved our dashboard's performance by:
- Using code splitting to break our bundle into smaller, manageable chunks.
- Applying lazy loading to defer the execution of heavy modules.
- Prioritizing modern, compressed image formats and native lazy loading.
As we continue to build, remember that performance is a continuous process. You've already learned how to handle state efficiently in Structuring State for Performance, and now your assets are optimized to match.
Up next: We'll dive into Internationalization (i18n) to make our dashboard accessible to a global audience.
Work with me

Next.js Full-Stack Web App Development
A fast, SEO-ready full-stack web app built with Next.js 16 โ from idea to deployed product, by an engineer who ships to production.

Next.js Website & Landing Page Development
A blazing-fast, SEO-optimized website or landing page in Next.js โ the kind that loads instantly and ranks. Design-to-code, done right.