I Cut My Next.js Page Load From 300ms to 90ms by Deleting Code
My homepage was loading in 300ms. I opened DevTools, found 180KB+ of unused CSS/JS shipped to marketing visitors, and fixed it in an afternoon. Here's the playbook.
My homepage was loading in 300ms. Not terrible. But I opened DevTools out of curiosity, and what I found made me feel stupid.
Every visitor -every person landing from Google, clicking a link on Twitter, skimming a blog post -was downloading a map library, a state management store, and 80KB of icon fonts. For a marketing page with zero maps, zero state, and three arrow icons.
I fixed it in an afternoon. 8 files. No new dependencies. Here's the playbook so you can do the same.
Check Your Root Layout Right Now
If you have a Next.js app with both marketing pages and an authenticated dashboard (most SaaS apps), open your root layout.tsx and look at the imports.
You'll probably find something like this:
import "./globals.css" // ← has CSS for dashboard-only libraries
import StoreProvider from "..." // ← wraps EVERYTHING, even landing pages
Your blog reader is downloading your dashboard's CSS. Your pricing page visitor is loading your Redux store. Your landing page is carrying the entire app on its back.
Here's what I found in mine and how I fixed each one.
1. CSS Imports in globals.css That Belong Elsewhere
The problem: I had @import "leaflet/dist/leaflet.css" and @import "leaflet-draw/dist/leaflet.draw.css" in my global stylesheet. Leaflet is a map library. My marketing pages have no maps.
The fix: Moved both imports into the dashboard layout (account/layout.tsx) -the only layout that renders maps.
Yours might be: Mapbox CSS, rich text editor styles, date picker themes, data table CSS. If it's only used behind a login, it doesn't belong in globals.
2. Providers Wrapping the Entire App
The problem: StoreProvider (Redux) was wrapping everything from the root layout. Marketing pages don't dispatch actions or read from a store. But they were still downloading and initializing Redux.
The fix: Moved StoreProvider down into account/layout.tsx and auth/layout.tsx. The root layout now just renders NextIntlClientProvider (lightweight) and children.
Yours might be: Redux, Zustand with heavy middleware, React Query with prefetched data, auth providers that fetch user sessions. Scope them to the routes that need them.
3. Loading an Icon Library for Three Icons
The problem: Font Awesome loaded from a CDN -80KB+ of CSS -so that three components could use <i className="fas fa-chevron-left">, <i className="fas fa-chevron-right">, and <i className="fas fa-arrow-up">.
The fix: Replaced those three with Lucide React components (<ChevronLeft />, <ChevronRight />, <ArrowUp />). Lucide was already in the bundle. Net addition: 0 bytes. The dashboard still needs Font Awesome, so I load it only in the dashboard layout.
Yours might be: Loading all of Material Icons for one spinner. Importing the entire Heroicons set for a hamburger menu. If you only use a handful of icons on marketing pages, use tree-shakeable React icon components instead.
4. Fonts Loaded via CDN Instead of next/font
The problem: Almarai font was loaded via a raw <link> tag pointing to Google Fonts CDN. That's a render-blocking external request -the browser has to connect to Google's servers, download the CSS, then download the font files, before it can render text.
The fix:
import { Almarai } from "next/font/google";
const almarai = Almarai({
weight: ["300", "400", "700"],
subsets: ["arabic"],
display: "swap",
variable: "--font-almarai",
});
next/font self-hosts the font at build time. No external request. No render blocking. Font is available immediately.
Yours might be: Any <link href="fonts.googleapis.com/..."> in your <head>. Replace all of them with next/font/google. It's a 5-minute change.
5. Duplicate or Stale Analytics Scripts
The problem: Google Analytics was loading twice. Once via a raw <script async> tag in <head> with one tracking ID. And again via a proper next/script component with another ID. Two different IDs. The first one was stale.
The fix: Removed the stale <script> tag. Kept the next/script component with the correct ID.
Yours might be: Duplicate Hotjar, Segment, Intercom, or Crisp scripts. Migration leftovers. Check your <head> -you might be loading the same tracker twice.
6. Unnecessary "use client" and React.memo
The problem: My homepage component was marked "use client" and wrapped in React.memo(). It's a page-level component rendered once per navigation. memo does nothing here. And "use client" forces Next.js to ship React hydration code for a component that could be a server component.
The fix: Removed both. The component is now a plain server component. Less JS shipped to the browser.
Yours might be: "use client" sprinkled liberally on components that don't use hooks, event handlers, or browser APIs. Every unnecessary "use client" is JS you're shipping for no reason.
The Result
Before: 300ms load. 180KB+ of unused CSS/JS shipped to marketing visitors.
After: ~90ms load. Marketing pages ship only what marketing pages need.
8 files changed. 66 lines added, 66 lines removed. Net zero lines of code.
The Quick Audit
Do this right now. It'll take 10 minutes:
- Open your root
layout.tsx. Count the imports. Ask: "Does my landing page need this?" - Open
globals.css. Look for@importstatements from dashboard-only libraries. - Search your codebase for
<link rel="stylesheet" href=-are you loading CDN stylesheets globally? - Search for
<scripttags in your<head>-any duplicates or stale tracking scripts? - Search for
"use client"-is it on components that don't use hooks or browser APIs?
Most performance wins aren't about doing something clever. They're about stopping something dumb that you didn't notice because it worked fine on your M2 MacBook with gigabit Wi-Fi.
Ship accordingly.
I'm Anas. I build Fleetzz. If this was useful, follow me -I write about building SaaS, Next.js, and the unglamorous work of making things fast.