<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aayush Bharti</title>
    <description>The latest articles on DEV Community by Aayush Bharti (@aayushbharti).</description>
    <link>https://dev.to/aayushbharti</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1069449%2Ff76d75ea-075a-40e3-b0cd-14d52884bd88.jpg</url>
      <title>DEV Community: Aayush Bharti</title>
      <link>https://dev.to/aayushbharti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aayushbharti"/>
    <language>en</language>
    <item>
      <title>How to Optimise a Next.js Web App</title>
      <dc:creator>Aayush Bharti</dc:creator>
      <pubDate>Tue, 14 Apr 2026 16:40:00 +0000</pubDate>
      <link>https://dev.to/aayushbharti/how-to-optimise-a-nextjs-web-app-47n1</link>
      <guid>https://dev.to/aayushbharti/how-to-optimise-a-nextjs-web-app-47n1</guid>
      <description>&lt;p&gt;&lt;a href="https://aayushbharti.in/blog/how-to-optimise-a-nextjs-web-app" rel="noopener noreferrer"&gt;https://aayushbharti.in/blog/how-to-optimise-a-nextjs-web-app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your Next.js app scores a 54 on Lighthouse. You shipped it three months ago with a perfect 100, and now there's an analytics SDK, a cookie banner, two icon libraries you imported wrong, and a client component wrapping your entire layout because someone needed &lt;code&gt;useState&lt;/code&gt; in the header. I've been there — more than once — and the fix is never one silver bullet. It's twenty small decisions compounding in the right direction.&lt;/p&gt;

&lt;p&gt;This is every optimisation technique I've used across production Next.js apps, ordered by how quickly you'll see results. No fluff, no "it depends" without telling you what it depends on. Let's fix your score.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Bundle size — the one that surprises everyone
&lt;/h2&gt;

&lt;p&gt;Before optimising anything, you need to know what you're shipping. Most Next.js apps are 2-3x larger than they need to be, and the culprit is almost never your code (I know, that hurts) — it's your dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.1 Analyse first, cut second
&lt;/h3&gt;

&lt;p&gt;Run the built-in analyzer (Next.js 16.1+):&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```bash title="Terminal"&lt;br&gt;
npx next experimental-analyze&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


![Bundle analyzer treemap showing package sizes](/blog/how-to-optimise-a-nextjs-web-app/bundle-analysis.webp)

You'll get a treemap showing exactly which packages eat the most space. Look for the usual suspects: `moment.js` (328KB — replace with `date-fns` or the native `Intl` API), full lodash imports, and icon libraries where you imported the entire set instead of individual icons.

### 1.2 The barrel export trap

Some packages export hundreds of modules from a single entry point — icon libraries, utility kits, component frameworks. You import one function and the bundler pulls in everything because it can't tree-shake inside `node_modules`.

Next.js has a fix for this. Add the package to `optimizePackageImports` and it rewrites your barrel imports to direct imports at build time — same developer experience, fraction of the bundle:



```ts title="next.config.ts"
const nextConfig = {
  experimental: {
    optimizePackageImports: ["@phosphor-icons/react", "recharts"], // [!code highlight]
  },
};
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Many popular libraries (&lt;code&gt;lodash-es&lt;/code&gt;, &lt;code&gt;date-fns&lt;/code&gt;, &lt;code&gt;@mui/material&lt;/code&gt;, and &lt;a href="https://nextjs.org/docs/app/api-reference/config/next-config-js/optimizePackageImports" rel="noopener noreferrer"&gt;others&lt;/a&gt;) are already optimised by default — check the list before adding them manually. I added two packages on this site and shaved ~180KB off the client bundle with zero code changes.&lt;/p&gt;
&lt;h3&gt;
  
  
  1.3 Server Components — stop shipping JS you don't need
&lt;/h3&gt;

&lt;p&gt;Every component in App Router is a Server Component by default — it ships zero JS to the browser. The mistake I see most often: marking an entire page as &lt;code&gt;"use client"&lt;/code&gt; because one small piece needs interactivity.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```tsx title="components/blog-post.tsx"&lt;br&gt;
"use client"; // Ships the entire page as JS // [!code --]&lt;/p&gt;

&lt;p&gt;export default function BlogPost({ post }) {&lt;br&gt;
  const [liked, setLiked] = useState(false); // State forces everything client-side // [!code --]&lt;br&gt;
  return (&lt;br&gt;
    &lt;br&gt;
      &lt;/p&gt;
&lt;h1&gt;{post.title}&lt;/h1&gt;
&lt;br&gt;
      &lt;p&gt;{post.content}&lt;/p&gt; {/* Static content — no reason to ship as JS &lt;em&gt;/}&lt;br&gt;
       setLiked(true)}&amp;gt;Like // [!code --]&lt;br&gt;
       {/&lt;/em&gt; Only this tiny piece ships JS */} // [!code ++]&lt;br&gt;
    &lt;br&gt;
  );&lt;br&gt;
}
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


Push `"use client"` as deep into the component tree as possible. The boundary should wrap the smallest interactive surface — a button, a form, a search input — not a page, not a layout.

&amp;lt;Callout type="warning" title="Common RSC pitfall"&amp;gt;

Passing a Server Component as `children` to a Client Component? It still runs on the server. This is how you compose interactive wrappers around static content without shipping the static content as JS.

&amp;lt;/Callout&amp;gt;

&amp;lt;Callout type="tip" title="Quick wins for bundle size"&amp;gt;

- Replace `moment` with `date-fns` or native `Intl.DateTimeFormat`
- Use specific imports for icon libraries, never `import * from`
- Audit with the bundle analyzer after every major dependency addition
- Target under 500KB total JS per page — 1500KB is the absolute ceiling

&amp;lt;/Callout&amp;gt;

## 2. Core Web Vitals and optimising FCP/LCP

Google uses four Core Web Vitals to rank your site. Here's what they actually mean and what "good" looks like:

| Metric | What it measures | Good | Needs work | Poor |
|---|---|---|---|---|
| **FCP** (First Contentful Paint) | Time until first text/image appears | &amp;lt; 1.8s | 1.8 - 3.0s | &amp;gt; 3.0s |
| **LCP** (Largest Contentful Paint) | Time until the largest visible element renders | &amp;lt; 2.5s | 2.5 - 4.0s | &amp;gt; 4.0s |
| **INP** (Interaction to Next Paint) | Delay between user interaction and visual response | &amp;lt; 200ms | 200 - 500ms | &amp;gt; 500ms |
| **CLS** (Cumulative Layout Shift) | How much the page layout shifts unexpectedly | &amp;lt; 0.1 | 0.1 - 0.25 | &amp;gt; 0.25 |

INP replaced FID (First Input Delay) in March 2024 — if you're still reading articles that reference FID, they're outdated.

### 2.1 Measure before you optimise

Run [PageSpeed Insights](https://pagespeed.web.dev/) on your production URL — not localhost, not a preview deployment. That's what Google actually measures.

![PageSpeed Insights showing 98 performance score with all green metrics](/blog/how-to-optimise-a-nextjs-web-app/pagespeed.webp)

For real-user data, check the [Chrome User Experience Report (CrUX)](https://developer.chrome.com/docs/crux/) — this is what Google uses for search rankings. For continuous monitoring, add [`@vercel/speed-insights`](https://vercel.com/docs/speed-insights) to your layout.

### 2.2 Images — the biggest LCP lever

`next/image` handles format conversion (WebP/AVIF), responsive sizing, and lazy loading automatically. Three things most people get wrong:

**1. Mark the hero image as `priority`.** Your LCP element is usually the largest above-the-fold image. By default, `next/image` lazy loads everything — the `priority` prop disables that and adds a `&amp;lt;link rel="preload"&amp;gt;` to the document head.



```tsx title="components/hero.tsx"
&amp;lt;Image
  src="/hero.webp"
  alt="Hero image"
  width={1200}
  height={630}
  priority // [!code highlight]
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;strong&gt;2. Use blur placeholders.&lt;/strong&gt; LQIP (Low Quality Image Placeholders) show a blurred preview instantly while the full image loads. Add &lt;code&gt;placeholder="blur"&lt;/code&gt; with a &lt;code&gt;blurDataURL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Don't lazy-load above-the-fold images.&lt;/strong&gt; If it's visible without scrolling, add &lt;code&gt;priority&lt;/code&gt; or &lt;code&gt;loading="eager"&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  2.3 Fonts — zero layout shift
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;next/font&lt;/code&gt; self-hosts fonts and eliminates external network requests. Use &lt;code&gt;display: "swap"&lt;/code&gt; so text renders immediately with a fallback, and &lt;code&gt;adjustFontFallback&lt;/code&gt; (enabled by default) calculates CSS overrides so the font swap causes zero CLS.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```tsx title="app/layout.tsx"&lt;br&gt;
import { Inter } from "next/font/google";&lt;/p&gt;

&lt;p&gt;const inter = Inter({ subsets: ["latin"], display: "swap" }); // [!code highlight]&lt;/p&gt;

&lt;p&gt;export default function RootLayout({ children }) {&lt;br&gt;
  return (&lt;br&gt;
    &lt;br&gt;
      &lt;/p&gt;{children}&lt;br&gt;
    &lt;br&gt;
  );&lt;br&gt;
}
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


### 2.4 Defer third-party scripts

Analytics, chat widgets, cookie banners — they all want to load during your critical rendering path. Push them out with `next/script`:



```tsx title="app/layout.tsx"
import Script from "next/script";

&amp;lt;Script
  src="https://analytics.example.com/script.js"
  strategy="lazyOnload" // Loads after everything else // [!code highlight]
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;When it loads&lt;/th&gt;
&lt;th&gt;Use for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;beforeInteractive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Before hydration&lt;/td&gt;
&lt;td&gt;Critical A/B testing, bot detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;afterInteractive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;After some hydration (default)&lt;/td&gt;
&lt;td&gt;Analytics, tag managers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lazyOnload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;After page is idle&lt;/td&gt;
&lt;td&gt;Chat widgets, social embeds, cookie banners&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For Google services, use &lt;a href="https://nextjs.org/docs/app/guides/third-party-libraries" rel="noopener noreferrer"&gt;&lt;code&gt;@next/third-parties&lt;/code&gt;&lt;/a&gt; — it loads GA, Maps, and YouTube embeds with optimised defaults out of the box.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;preconnect&lt;/code&gt; hints for third-party origins — each one saves 100-500ms of DNS + TCP + TLS handshake time:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```tsx title="app/layout.tsx"&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


## 3. Rendering strategies

Choosing the right rendering strategy has a direct impact on TTFB, FCP, and LCP.

| Strategy | How it works | TTFB | Use when |
|---|---|---|---|
| **SSG** | HTML generated at build time, served from CDN | Fastest | Landing pages, docs, blogs — content rarely changes |
| **ISR** | Static + revalidates at a fixed interval | Fast | Product listings, content that changes every few minutes/hours |
| **SSR** | HTML generated per request | Depends on backend | SEO-critical pages with real-time or personalised data |
| **CSR** | Renders entirely in browser | N/A | Dashboards, internal tools — SEO doesn't matter |



```mermaid
graph LR
    A{Needs SEO?} --&amp;gt;|No| CSR["CSR\nClient-Side"]
    A --&amp;gt;|Yes| B{Per-request\ndata?}
    B --&amp;gt;|Yes| SSR["SSR\nServer-Side"]
    B --&amp;gt;|No| C{Updates\nperiodically?}
    C --&amp;gt;|Yes| ISR["ISR\nIncremental"]
    C --&amp;gt;|No| SSG["SSG\nStatic"]
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Start with SSG. Move to ISR if data needs freshness. Move to SSR only if data needs per-request accuracy. CSR is a last resort. If you're on Next.js 16+, look at Partial Prerendering (PPR) — it serves a static shell instantly and streams dynamic sections, combining the best of SSG and SSR in a single page.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Code splitting and dynamic imports
&lt;/h2&gt;

&lt;p&gt;Next.js splits code at the route level automatically — each page only loads the JavaScript it needs. But heavy components within a page still land in that page's bundle unless you split them manually (the bundler is helpful, not psychic).&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;next/dynamic&lt;/code&gt; for components that are heavy, below the fold, or client-only:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```tsx title="components/dashboard-charts.tsx"&lt;br&gt;
"use client"; // ssr: false only works in Client Components&lt;/p&gt;

&lt;p&gt;import dynamic from "next/dynamic";&lt;/p&gt;

&lt;p&gt;const Chart = dynamic(() =&amp;gt; import("@/components/chart"), {&lt;br&gt;
  ssr: false, // Skip server render — this uses browser APIs // [!code highlight]&lt;br&gt;
  loading: () =&amp;gt; &lt;/p&gt;,&lt;br&gt;
});
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


**Use dynamic imports for:** heavy client libraries (charts, editors), browser-only APIs (`window`, `document`), below-the-fold content most users never scroll to.

**Don't use them for:** small shared components, above-the-fold UI, layout components. Every dynamic import creates a separate network request — splitting ten small components into ten chunks is worse than one bundle.

## 5. Data fetching and caching

Slow data fetching is the quiet one that bites you. Your rendering strategy doesn't matter if you're waterfalling three sequential API calls before the page can render.

### 5.1 Parallel data fetching

The most common mistake: sequential `await`s when the calls don't depend on each other.



```ts title="Don't do this — sequential waterfall"
const user = await getUser();       // 200ms
const posts = await getPosts();     // 300ms
const comments = await getComments(); // 150ms
// Total: 650ms — each waits for the previous one
&lt;/code&gt;&lt;/pre&gt;




&lt;p&gt;```ts title="Do this — parallel fetching"&lt;br&gt;
const [user, posts, comments] = await Promise.all([ // [!code highlight]&lt;br&gt;
  getUser(),       // 200ms ─┐&lt;br&gt;
  getPosts(),      // 300ms ─┤ All start simultaneously&lt;br&gt;
  getComments(),   // 150ms ─┘&lt;br&gt;
]);&lt;br&gt;
// Total: 300ms — limited by the slowest call&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


54% faster from one line. No library, no config — `Promise.all` and done.

### 5.2 Request deduplication with `cache()`

React's `cache()` deduplicates identical requests within a single render pass. If three components all call `getUser()`, it executes once.



```ts title="lib/data.ts"
import { cache } from "react";

export const getUser = cache(async (id: string) =&amp;gt; { // [!code highlight]
  const res = await fetch(`/api/users/${id}`);
  return res.json();
});
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;
  
  
  5.3 The &lt;code&gt;"use cache"&lt;/code&gt; directive
&lt;/h3&gt;

&lt;p&gt;This is the big one that almost no blog covers yet. &lt;code&gt;"use cache"&lt;/code&gt; is a declarative caching directive — first introduced experimentally in Next.js 15 and enabled via Cache Components in Next.js 16. It replaces the old &lt;code&gt;fetch()&lt;/code&gt; cache options and &lt;code&gt;unstable_cache&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, enable it in your config:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```ts title="next.config.ts"&lt;br&gt;
const nextConfig = {&lt;br&gt;
  cacheComponents: true, // [!code highlight]&lt;br&gt;
};&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


Then use it at the page level, layout level, or individual functions:



```tsx title="app/blog/page.tsx"
"use cache";

import { cacheLife } from "next/cache";

export default async function BlogPage() {
  cacheLife("hours"); // Cache this page's output for hours // [!code highlight]
  const posts = await getAllPosts();
  return &amp;lt;PostList posts={posts} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;The built-in cache profiles:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;Stale&lt;/th&gt;
&lt;th&gt;Revalidate&lt;/th&gt;
&lt;th&gt;Expire&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"default"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5min&lt;/td&gt;
&lt;td&gt;15min&lt;/td&gt;
&lt;td&gt;never&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"seconds"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30s&lt;/td&gt;
&lt;td&gt;1s&lt;/td&gt;
&lt;td&gt;1min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"minutes"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5min&lt;/td&gt;
&lt;td&gt;1min&lt;/td&gt;
&lt;td&gt;1hr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"hours"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5min&lt;/td&gt;
&lt;td&gt;1hr&lt;/td&gt;
&lt;td&gt;1 day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"days"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5min&lt;/td&gt;
&lt;td&gt;1 day&lt;/td&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"weeks"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5min&lt;/td&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"max"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5min&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;1 year&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you don't call &lt;code&gt;cacheLife()&lt;/code&gt; at all, the &lt;code&gt;default&lt;/code&gt; profile is used. For on-demand revalidation, pair it with &lt;code&gt;cacheTag()&lt;/code&gt; and call &lt;code&gt;revalidateTag()&lt;/code&gt; from an API route or Server Action. This replaces the old route segment configs (&lt;code&gt;revalidate&lt;/code&gt;, &lt;code&gt;dynamic&lt;/code&gt;, &lt;code&gt;fetchCache&lt;/code&gt;) — don't mix both models.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. Streaming and Suspense
&lt;/h2&gt;

&lt;p&gt;Traditional SSR waits for the slowest data source before sending anything. If your content loads in 100ms but comments take 2 seconds, the user stares at a blank screen for 2 seconds.&lt;/p&gt;

&lt;p&gt;Streaming fixes this — the server sends fast parts immediately and streams slow parts as they resolve:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```tsx title="app/blog/[slug]/page.tsx"&lt;br&gt;
import { Suspense } from "react";&lt;/p&gt;

&lt;p&gt;export default async function BlogPost({ params }) {&lt;br&gt;
  const { slug } = await params;&lt;br&gt;
  const post = await getPost(slug); // Fast — 50ms&lt;/p&gt;

&lt;p&gt;return (&lt;br&gt;
    &lt;br&gt;
      &lt;/p&gt;
&lt;h1&gt;{post.title}&lt;/h1&gt;
&lt;br&gt;
      &lt;p&gt;{post.content}&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  &amp;lt;Suspense fallback={&amp;lt;CommentsSkeleton /&amp;gt;}&amp;gt; // [!code highlight]
    &amp;lt;Comments slug={slug} /&amp;gt; {/* Streams in when ready — 800ms */}
  &amp;lt;/Suspense&amp;gt;
&amp;lt;/article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;);&lt;br&gt;
}&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


Place Suspense boundaries around non-critical data fetchers, below-the-fold sections, and personalised content. Don't wrap above-the-fold content — a loading flash there hurts perceived performance more than it helps.

For page-level streaming, you can also use a `loading.tsx` file — Next.js wraps the page in a Suspense boundary for you automatically:



```tsx title="app/dashboard/loading.tsx"
export default function Loading() {
  return &amp;lt;DashboardSkeleton /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This is the simplest way to get streaming — one file, zero Suspense imports, instant loading states for entire route segments.&lt;/p&gt;
&lt;h2&gt;
  
  
  7. React Compiler
&lt;/h2&gt;

&lt;p&gt;Here's a technique zero performance articles talk about (I checked): stop memoising things manually.&lt;/p&gt;

&lt;p&gt;React Compiler analyses your components at build time and automatically inserts &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;, and &lt;code&gt;React.memo&lt;/code&gt; where they'll actually help — not where you think they'll help, but where static analysis proves it.&lt;/p&gt;

&lt;p&gt;First, install the Babel plugin:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```bash title="Terminal"&lt;br&gt;
pnpm add -D babel-plugin-react-compiler&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


Then enable it in your config — note this is a **top-level** option, not inside `experimental`:



```ts title="next.config.ts"
const nextConfig = {
  reactCompiler: true, // [!code highlight]
};
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;In most cases, you can remove your manual &lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt;/&lt;code&gt;React.memo&lt;/code&gt; calls — the compiler analyses the actual dependency graph at build time instead of relying on you listing deps correctly in an array. If a specific component needs to opt out, use the &lt;code&gt;"use no memo"&lt;/code&gt; directive. Fewer unnecessary re-renders means better INP.&lt;/p&gt;
&lt;h2&gt;
  
  
  8. next.config power flags
&lt;/h2&gt;

&lt;p&gt;These are the flags I run in production that most developers don't know exist. (Free performance. No code changes. You're welcome.)&lt;/p&gt;
&lt;h3&gt;
  
  
  8.1 &lt;code&gt;inlineCss&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Inlines CSS directly into HTML instead of serving it as separate files. Eliminates render-blocking CSS requests.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```ts title="next.config.ts"&lt;br&gt;
const nextConfig = {&lt;br&gt;
  experimental: {&lt;br&gt;
    inlineCss: true, // [!code highlight]&lt;br&gt;
  },&lt;br&gt;
};&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


One fewer network round-trip per page load. Only works in production builds. The tradeoff: inlined CSS can't be cached separately, so returning visitors re-download styles — best for first-visit-heavy sites like landing pages and blogs.

### 8.2 `staleTimes`

Controls how long the client-side router caches visited pages. By default, dynamic pages are cached for 0 seconds (re-fetched on every navigation) and static pages for 5 minutes.



```ts title="next.config.ts"
const nextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 30,  // Cache dynamic pages for 30s on client // [!code highlight]
      static: 180,  // Cache static pages for 3 min on client
    },
  },
};
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This means navigating back to a previously visited page is instant for 30 seconds instead of triggering a new server request. Big win for apps with frequent back-and-forth navigation.&lt;/p&gt;
&lt;h3&gt;
  
  
  8.3 &lt;code&gt;serverExternalPackages&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Some Node.js packages break when bundled for Server Components — native bindings, packages that use &lt;code&gt;__dirname&lt;/code&gt;, or packages with side effects during import. This flag tells Next.js to skip bundling and use native &lt;code&gt;require()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```ts title="next.config.ts"&lt;br&gt;
const nextConfig = {&lt;br&gt;
  serverExternalPackages: ["puppeteer", "canvas"],&lt;br&gt;
};&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


Many common packages (`sharp`, `bcrypt`, `prisma`, and others) are already excluded by default — you only need this for packages not on the [automatic opt-out list](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages).

### 8.4 `removeConsole`

Strip `console.log` statements from production builds. Less noise, slightly smaller bundles.



```ts title="next.config.ts"
const nextConfig = {
  compiler: {
    removeConsole: {
      exclude: ["error"], // Keep console.error for debugging // [!code highlight]
    },
  },
};
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;
  
  
  9. Production checklist
&lt;/h2&gt;

&lt;p&gt;Before you ship, run through this. I've ordered by impact — fix the high-priority items first.&lt;/p&gt;
&lt;h3&gt;
  
  
  High Priority
&lt;/h3&gt;



&lt;h3&gt;
  
  
  Medium Priority
&lt;/h3&gt;



&lt;h3&gt;
  
  
  Low Priority
&lt;/h3&gt;






&lt;p&gt;Here's the thing nobody tells you about performance work: hitting a 100 on Lighthouse is easy. Staying there is the actual job.&lt;/p&gt;

&lt;p&gt;Every feature you ship, every dependency you add, every "just this one client component" — they're all small withdrawals from a budget your users never agreed to. The sites that stay fast aren't the ones that optimised once. They're the ones that made performance a constraint, not a cleanup task.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;@vercel/speed-insights&lt;/code&gt; to your layout. Set a bundle budget in CI. Make the number visible to your team every week. When someone asks "can we add this 200KB carousel library?" — the dashboard answers for you.&lt;/p&gt;

&lt;p&gt;The best Lighthouse score is the one you never have to fix twice.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>performance</category>
      <category>react</category>
      <category>seo</category>
    </item>
  </channel>
</rss>
