Optimizing the performance of a React application is crucial for providing a smooth and responsive user experience. One way to achieve this is by implementing techniques like lazy loading, caching and code splitting.
Lazy loading is a runtime behavior. It delays the loading of components until they are needed. This reduces the initial bundle size and improves page load times.
React provides built-in support through React.lazy
and Suspense
.
import React, { Suspense } from "react"
const Settings = React.lazy(() => import("./Settings"))
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<Settings />
</Suspense>
</div>
)
}
Only when the Settings
component is rendered will the associated chunk be downloaded.
Code splitting breaks the app into smaller bundles that can be loaded on demand. This avoids shipping unnecessary code up front.
With tools like Webpack and Vite, you can split code by routes, components, or even libraries.
import { lazy, Suspense } from "react"
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"
const Home = lazy(() => import("./pages/Home"))
const About = lazy(() => import("./pages/About"))
function AppRoutes() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</Router>
)
}
Although Lazy Loading and Code Splitting appear similar, they are in truth 2 different concepts. The often work together but are not to be confused as the same thing.
Concept | Code Splitting | Lazy Loading |
---|---|---|
When | Build time | Runtime |
What | Splits code into smaller files | Loads those files only when needed |
How | Uses dynamic import() |
Uses React.lazy / Suspense , etc. |
Purpose | Reduce bundle size | Improve runtime performance |
Relation | Enables lazy loading | Depends on code splitting to work |
Caching is the process of storing a copy of data (like files, API responses, or assets) in a temporary location so it can be accessed faster in the future.
Instead of fetching data or reloading resources every time, the app reuses the cached version — saving time, bandwidth, and processing power.
Caching isn’t one-size-fits-all. These are the most common strategies, each suited for different scenarios:
Cache First
How it works: Check the cache. If the resource is found, use it. If not, fetch from the network and cache it.
Use case: Static assets like images, fonts, or bundled JS/CSS.
Pros: Fast load times for repeat visits. Works offline.
Cons: Risk of stale content if not updated.
Network First
How it works: Try to fetch from the network. If that fails (e.g., offline), fall back to cache.
Use case: Dynamic data like user dashboards or feeds.
Pros: Keeps content fresh.
Cons: Slower than cache-first, and fails without a network if not cached yet.
Stale While Revalidate
How it works: Serve from cache immediately, then update the cache in the background with fresh data.
Use case: Pages that should load fast but don’t need to be 100% up to date.
Pros: Instant response + eventual freshness.
Cons: Can show outdated data temporarily.
Cache Only/Network Only
Cache Only: Only load from cache. Good for true offline-first apps.
Network Only: Always fetch from the server. Used for admin panels or sensitive real-time data.
Layer | Tool / Method | Typical Use |
---|---|---|
HTTP Caching | Cache-Control , ETag , Expires |
Static assets (CDN or server) |
App Caching | localStorage , IndexedDB |
Store API data, auth, settings |
Service Worker | Workbox, custom fetch handlers | Control request/response logic |
Optimizing web applications doesn’t require major rewrites most times. It’s all about reducing what the browser loads, when it loads it, and how often it reloads the same content. These techniques make your app feel faster without changing the user experience or design.