A React application’s load time is determined largely by the size of its JavaScript bundle. Every library added to package.json potentially increases bundle size. The goal is to ship only the code needed for the current page — the home page should not download the post editor code, and neither page should download libraries that are already cached from a previous visit. Vite’s code splitting (via dynamic imports and manual chunks) and proper HTTP caching headers together achieve this.
Bundle Analysis
npm install -D rollup-plugin-visualizer
// vite.config.js — add visualizer plugin
import { visualizer } from "rollup-plugin-visualizer";
export default defineConfig({
plugins: [
react(),
visualizer({
open: true, // open in browser after build
filename: "dist/stats.html",
gzipSize: true,
template: "treemap", // or "sunburst", "network"
}),
],
build: {
rollupOptions: {
output: {
manualChunks: {
// Separate vendor chunk — cached separately by browser
"vendor-react": ["react", "react-dom", "react-router-dom"],
"vendor-redux": ["@reduxjs/toolkit", "react-redux"],
"vendor-ui": ["@headlessui/react", "clsx"],
},
},
},
},
});
Note: Manual chunk splitting separates vendor libraries (React, Redux, router) from your application code. Browser caching works on file hashes — when you deploy a new version of the app, only the application chunks change (new hashes). The vendor chunks, which rarely change, keep their old hashes and are served from the browser cache. A user who visited your site last week gets the new app code but reuses their cached React bundle — saving ~150KB of network transfer per visit.
Tip: Measure Core Web Vitals before and after optimisations with Lighthouse (built into Chrome DevTools). The three most important metrics for a blog: LCP (Largest Contentful Paint) — time until the main content is visible, target < 2.5s; CLS (Cumulative Layout Shift) — visual stability, target < 0.1; INP (Interaction to Next Paint) — response to user input, target < 200ms. LCP is typically improved by reducing bundle size and lazy-loading non-critical images; CLS is fixed by giving images explicit dimensions.
Warning:
React.memo adds a shallow equality comparison on every render — this is only beneficial if the comparison is cheaper than the re-render it prevents. For small, fast-rendering components like Badge or Avatar, React.memo adds overhead without benefit. Profile with React DevTools before adding memo — memoising components that render cheaply can actually make the application slower. Use memo for expensive components (large lists, complex charts) that receive the same props often.React.memo for List Items
import { memo, useCallback } from "react";
// Wrap PostCard in memo — prevents re-render when parent re-renders
// but this PostCard's own props haven't changed
const PostCard = memo(function PostCard({ post, onLike }) {
// Component body
return <article>...</article>;
}, (prevProps, nextProps) => {
// Custom comparison: only re-render if post.id or post.like_count changes
return prevProps.post.id === nextProps.post.id
&& prevProps.post.like_count === nextProps.post.like_count
&& prevProps.post.title === nextProps.post.title;
});
// In PostList — pass stable callbacks to avoid breaking memo
function PostList({ posts }) {
const handleLike = useCallback(async (postId) => {
// ... like logic
}, []); // ← stable reference — PostCard won't re-render due to onLike change
return posts.map(post =>
<PostCard key={post.id} post={post} onLike={handleLike} />
);
}
Code Splitting with React.lazy
// Already covered in Chapter 37 — reminder of the impact
// Without lazy: entire app in one bundle → ~450KB gzipped
// With lazy: home page bundle → ~80KB gzipped
// post editor loaded only when navigating to /posts/new → +~60KB
// The initial page load is 5.6× faster for new users who never need the editor
Core Web Vitals Checklist
| Metric | Target | Common Fix |
|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | Reduce bundle size, preload hero image, use CDN |
| CLS (Cumulative Layout Shift) | < 0.1 | Explicit image dimensions, avoid font swap |
| INP (Interaction to Next Paint) | < 200ms | Debounce event handlers, avoid main-thread work on click |
| TTFB (Time to First Byte) | < 600ms | Server response caching, CDN for static assets |