This episode covers profiling with React DevTools and browser tools, code splitting with bundle analysis and tree shaking, Core Web Vitals optimization, and reducing JavaScript payload using server components and dynamic import.

Performance optimization isn't about guessing: it takes measurement, diagnosis, then measurable improvement. In episode 15 we build that process from the ground up — from profiling, bundle analysis, and Web Vitals metrics, to concrete techniques for reducing the JavaScript sent to the browser.
The first step is to measure. React DevTools shows the rendering profile: which components render most often and how long each render takes. The browser DevTools (Chrome/Edge) provide a Performance panel for recording load, network, and rendering events comprehensively.
The recommended flow: record a slow interaction, watch the profiler, find the components consuming time, then fix and re-measure. Without an initial measurement, you're only optimizing randomly.
Build the habit of measuring at every milestone: before starting a feature, record a baseline; after finishing, compare the results. This builds a performance culture in the team instead of waiting for problems to appear.
A good profile starts with a stable reproduction: use the same device and note network conditions so comparisons across sessions stay fair.
Some situations that often appear in profiles: components re-rendering because new object props are created on every render, inline event handlers that change references, and state stored too high in the component tree. Memoization with useMemo and useCallback helps, but only after measuring — don't optimize without data.
Next.js already performs code splitting per route — only the JavaScript for the active page is sent. To verify, install @next/bundle-analyzer and run a build with analysis:
npm install --save-dev @next/bundle-analyzer
npx next build --analyzeThe command npx next build --analyze opens a visual report of each bundle's size. From here you can see which libraries contribute the most size — usually charts, editors, or date pickers.
Tree shaking removes unused code from the bundle. Practices that support it: use named imports from a library rather than import * as, avoid side effects on import, and choose libraries designed to be tree-shakeable. The bundle analyzer also reveals code that should have been trimmed.
A recommended routine: run the bundle analyzer before and after adding a new dependency. If a library adds 50 kB for a rarely used feature, find an alternative or move it to a dynamic import.
Beyond per-route size, also watch the number of requests: too many small chunks can also slow things down. The balance between the count and size of chunks is part of the art of bundle optimization.
Core Web Vitals is the user experience standard measured by Google:
The first three are Core Web Vitals; TTFB is a supporting metric that affects LCP. Optimizing LCP images, explicit dimensions to prevent CLS, and reducing JavaScript for INP are the three improvements with the biggest impact. Web vitals scores are also affected by infrastructure: choose a platform with an edge network and HTTP/3 to cut TTFB, especially for users across countries.
INP measures the latency of user interactions: from a click, tap, or scroll, until a response is visible. A common cause of poor INP is heavy JavaScript blocking the main thread. Reduce main-thread work by handing rendering to server components and limiting unnecessary state updates. CLS, meanwhile, usually comes from images without dimensions, fonts that load late, and ads inserted after content.
next/font so text doesn't shift when fonts load.The App Router provides the reportWebVitals function for sending metrics to an analytics service:
"use client"
export function reportWebVitals(metric) {
console.log(metric.name, metric.value)
}The function reportWebVitals(metric) is called every time a metric is measured. Send the values to your own endpoint or a service like Vercel Analytics — observability will be covered fully in episode 22.
Server components don't send JavaScript to the browser — moving rendering to the server drastically reduces the payload. For heavy client components, use dynamic import so they load only when needed:
import dynamic from "next/dynamic"
const Chart = dynamic(() => import("@/components/Chart"), {
ssr: false,
loading: () => <p>Memuat grafik...</p>,
})The dynamic() above splits the Chart component into a separate bundle downloaded only when rendered. The ssr: false option prevents a heavy component from being rendered on the server too. This strategy keeps the whole page from paying the cost of a component that's rarely seen.
Combining server components, dynamic import, and library restrictions produces a drastic payload reduction. A healthy target: under 150 kB of JavaScript per route for content pages, the smaller the better. Measure with the bundle analyzer after every major change so size trends don't silently worsen. Also watch large JSON imported directly from files — it gets bundled; move large data to an API or build a pipeline that only includes the needed fields.
In addition, the React Compiler adopted by Next.js automates memoization so much manual useMemo becomes unnecessary. This feature reduces the work developers have to maintain and makes performance optimization simpler.
Here's what to take away:
In the next episode, episode 16, we'll discuss testing and quality — unit testing with Jest and React Testing Library, integration and E2E testing with Playwright or Cypress, accessibility testing, and static analysis with ESLint and type checking. Your application's quality will be guaranteed by automation.