# Aayush Bharti — Full Content > Full-Stack Developer specializing in Next.js, React, TypeScript, and Sanity CMS. I build fast, accessible web apps and help founders ship products that users love. Website: https://aayushbharti.in Email: hello@aayushbharti.in GitHub: https://github.com/aayushbharti LinkedIn: https://linkedin.com/in/iaayushbharti --- # About I'm Aayush Bharti, a proactive full-stack developer passionate about creating dynamic web experiences. From frontend to backend, I thrive on solving complex problems with clean, efficient code. My expertise spans React, Next.js, and Node.js, and I'm always eager to learn more. --- # Blog Posts ## How to Optimise a Next.js Web App > Practical techniques to fix your Next.js Lighthouse score — bundle analysis, caching strategies, React Compiler, and the next.config flags nobody talks about. - Author: Aayush Bharti - Published: Tue Apr 14 2026 00:00:00 GMT+0000 (Coordinated Universal Time) - Updated: Wed May 13 2026 00:00:00 GMT+0000 (Coordinated Universal Time) - Tags: nextjs, performance, react, web-vitals - Reading time: 15 min - URL: https://aayushbharti.in/blog/how-to-optimise-a-nextjs-web-app 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 `useState` 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. 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. ## 1. Bundle size — the one that surprises everyone 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. ### 1.1 Analyse first, cut second Run the built-in analyzer (Next.js 16.1+): ```bash title="Terminal" npx next experimental-analyze ``` ![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] }, }; ``` Many popular libraries (`lodash-es`, `date-fns`, `@mui/material`, and [others](https://nextjs.org/docs/app/api-reference/config/next-config-js/optimizePackageImports)) 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. ### 1.3 Server Components — stop shipping JS you don't need 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 `"use client"` because one small piece needs interactivity. ```tsx title="components/blog-post.tsx" "use client"; // Ships the entire page as JS // [!code --] export default function BlogPost({ post }) { const [liked, setLiked] = useState(false); // State forces everything client-side // [!code --] return (

{post.title}

{post.content}

{/* Static content — no reason to ship as JS */} // [!code --] {/* Only this tiny piece ships JS */} // [!code ++]
); } ``` 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. > **Common RSC pitfall** > > 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. > **Quick wins for bundle size** > > - 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 ## 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 | < 1.8s | 1.8 - 3.0s | > 3.0s | | **LCP** (Largest Contentful Paint) | Time until the largest visible element renders | < 2.5s | 2.5 - 4.0s | > 4.0s | | **INP** (Interaction to Next Paint) | Delay between user interaction and visual response | < 200ms | 200 - 500ms | > 500ms | | **CLS** (Cumulative Layout Shift) | How much the page layout shifts unexpectedly | < 0.1 | 0.1 - 0.25 | > 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 `` to the document head. ```tsx title="components/hero.tsx" Hero image ``` **2. Use blur placeholders.** LQIP (Low Quality Image Placeholders) show a blurred preview instantly while the full image loads. Add `placeholder="blur"` with a `blurDataURL`. **3. Don't lazy-load above-the-fold images.** If it's visible without scrolling, add `priority` or `loading="eager"`. ### 2.3 Fonts — zero layout shift `next/font` self-hosts fonts and eliminates external network requests. Use `display: "swap"` so text renders immediately with a fallback, and `adjustFontFallback` (enabled by default) calculates CSS overrides so the font swap causes zero CLS. ```tsx title="app/layout.tsx" import { Inter } from "next/font/google"; const inter = Inter({ subsets: ["latin"], display: "swap" }); // [!code highlight] export default function RootLayout({ children }) { return ( {children} ); } ``` ### 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";