Performance is not just about making things fast β itβs about making them feel fast.
In real-world applications, users donβt care if your API takes 300ms or 800ms. They care about:
- How quickly content appears
- Whether the UI feels responsive
- If things jump around unexpectedly
Thatβs why frontend performance should be approached in three layers:
π Network β Rendering β User Perception
And validated using:
π Core Web Vitals: LCP, CLS, INP
1. Network Optimization
Network is the first bottleneck. Before React even runs, the browser must download HTML, CSS, JS, images, and fonts.
The goal here is simple:
π Send less data, and send it only when needed
Route-based Code Splitting
Instead of shipping your entire app in one bundle, you split it by routes. This ensures users only download code for the page they visit.
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
);
}
This is often the single biggest performance win in large applications because it dramatically reduces initial bundle size.
Component-level Lazy Loading
Not everything on a page needs to load immediately. Heavy components like editors, charts, or modals can be loaded only when required.
const RichEditor = lazy(() => import('./components/RichEditor'));
function PostComposer({ isEditing }) {
if (!isEditing) return <PostPreview />;
return (
<Suspense fallback={<EditorSkeleton />}>
<RichEditor />
</Suspense>
);
}
This avoids loading unnecessary code during initial render, improving both load time and memory usage.
Preload on Hover
You can go one step further by predicting user intent. If a user hovers over a link, thereβs a high chance they will click it.
const preloadDashboard = () => import('./pages/Dashboard');
<NavLink to="/dashboard" onMouseEnter={preloadDashboard}>
Dashboard
</NavLink>
This makes navigation feel instant, even though the code is still lazily loaded.
Vendor Splitting & Tree Shaking
Dependencies like React or libraries donβt change frequently. By separating them into vendor chunks, they can be cached longer.
Tree shaking ensures you only import what you actually use.
import { debounce } from 'lodash-es'; // good
import _ from 'lodash'; // bad
Together, these reduce bundle size and improve caching efficiency.
Compression
Modern compression like Brotli can reduce bundle sizes significantly compared to gzip. This directly improves download time, especially on slower networks.
Request Optimization
Network performance isnβt just about bundles β itβs also about API calls.
For example, without debouncing, every keystroke in a search box can trigger a network request. Thatβs wasteful and can overload both client and server.
function useDebounce(value, delay) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value]);
return debounced;
}
Similarly, throttling ensures that high-frequency events like scrolling donβt overwhelm the browser.
Using caching, batching, and CDNs further reduces unnecessary network overhead.
Asset Optimization
Images are usually the heaviest resources in an application.
Using modern formats like WebP or AVIF, along with responsive images and lazy loading, can drastically reduce load times.
<img src="/hero.webp" loading="lazy" decoding="async" />
The key idea is simple:
π Load the right asset, at the right time, in the right size
2. Rendering Optimization
Once the data is downloaded, the next bottleneck is the browser itself β specifically React rendering and DOM updates.
Here the goal is:
π Avoid unnecessary work
Memoization
React re-renders components whenever state or props change. Sometimes this leads to unnecessary recalculations or re-renders.
const sorted = useMemo(() => users.sort(), [users]);
const handler = useCallback(() => {}, []);
const Card = React.memo(({ user }) => <div>{user.name}</div>);
-
useMemoavoids recomputing expensive values -
useCallbackkeeps function references stable -
React.memoskips re-rendering unchanged components
However, memoization itself has a cost β so use it only where it actually helps.
State Management Optimization
A common mistake is subscribing to the entire state, which causes re-renders on every update.
// bad
const store = useStore();
// good
const count = useStore(state => state.count);
By selecting only what you need, you reduce unnecessary updates and improve performance.
Virtualization
Rendering large lists (100+ items) can be expensive because each item adds to the DOM.
Virtualization solves this by rendering only the visible portion of the list and reusing DOM elements during scrolling.
This dramatically reduces both rendering time and memory usage.
Avoid Inline Objects
Inline objects or arrays create new references on every render, breaking memoization.
// bad
<Chart options={{ color: 'blue' }} />
// good
const options = useMemo(() => ({ color: 'blue' }), []);
Stable references ensure React can properly optimize rendering.
Memory Cleanup
Uncleaned timers, subscriptions, or listeners can lead to memory leaks and performance degradation over time.
useEffect(() => {
const id = setInterval(fetchData, 5000);
return () => clearInterval(id);
}, []);
Always clean up side effects to keep your app efficient.
3. User Perceived Performance
Even if your app is technically fast, users may still feel itβs slow.
This layer focuses on:
π Making the app feel fast
Skeleton UI
Instead of showing a spinner, display a layout placeholder that resembles the actual content.
This reduces perceived waiting time because users can visually process structure immediately.
Blur-up Images
Load a low-quality placeholder first, then replace it with a high-resolution image.
This creates a smooth transition and avoids blank spaces during loading.
Predictive Fetching
You can preload resources based on user behavior:
- Hover β likely navigation
- Viewport β content about to appear
- Analytics β predicted next action
This makes interactions feel instant.
Third-party Scripts
Analytics, chat widgets, and A/B testing tools are often the biggest performance bottlenecks.
Instead of loading them immediately:
- Defer them
- Load after user interaction
- Use a lightweight placeholder (facade pattern)
4. Resource Hints
Browsers allow you to guide how resources should be loaded.
<link rel="preload" href="/hero.webp" as="image" />
<link rel="preconnect" href="https://api.example.com" />
<link rel="prefetch" href="/next.js" />
- Preload β critical resources needed now
- Preconnect β establish connection early
- Prefetch β load resources for future navigation
Used correctly, these can significantly improve loading performance.
5. Core Web Vitals
These metrics reflect real user experience.
LCP (Largest Contentful Paint)
Measures how quickly the main content loads.
π Target: < 2.5s
Improve by:
- Optimizing images
- Using CDN
- Reducing render-blocking resources
CLS (Cumulative Layout Shift)
Measures visual stability β how much elements move unexpectedly.
π Target: < 0.1
Fix by:
- Setting image dimensions
- Reserving layout space
- Avoiding dynamic shifts
INP (Interaction to Next Paint)
Measures responsiveness to user interactions.
π Target: < 200ms
requestAnimationFrame(() => updateUI());
requestIdleCallback(() => sendAnalytics());
Keep event handlers lightweight and defer non-critical work.
6. Low-End Device Considerations
Not all users have high-end devices.
- Prefer
transformandopacityfor animations - Avoid expensive CSS properties
- Respect
prefers-reduced-motion
Designing for low-end devices ensures your app works well for everyone.
7. Advanced Techniques
For large-scale applications:
- SSR / Streaming β faster initial render
- React Server Components β reduce client JS
- Web Workers β move heavy work off main thread
- Service Workers β caching and offline support
8. Profiling Workflow
Performance optimization is iterative.
- Measure using React Profiler / Chrome DevTools
- Identify bottlenecks
- Apply fixes
- Measure again
π Repeat this loop continuously.
π‘ Interview Answer
βI optimize performance in three layers:
- Network (code splitting, caching)
- Rendering (memoization, virtualization)
- User perception (skeletons, prefetching)
And validate using Core Web Vitals: LCP, CLS, INP.β
π₯ Final Thought
Performance is not about micro-optimizations.
Itβs about eliminating unnecessary work.
π The fastest code is the code that never runs.



Top comments (0)