So I recently went down a bit of a rabbit hole optimising my Next.js app, and honestly? It was crazy. I wanted to share what I learned because there were some surprises along the way, and maybe it will help someone else trying to speed up their site or it will stir up a nice discussion. Either way, it's a win-win situation for me π.
Disclaimer - the things I learned are both from research (google, stackoverflow, dev.to, shady techy websites) and, of course, AI. The cover image is AI generated using Gemini.
The Big Plot Twist
Here's the thing that almost made me lose my mind: my local production build was scoring like 75/100 on Lighthouse. I was freaking out trying everything, questioning my very own existence...π₯΄ and then I deployed it to actual production and...it hit 98/100. How about that?!
Turns out local builds can be super misleading. Your local machine, the way Next.js serves files locally, network conditions β it is all very different from real production. I don't really know the ins and outs of it but the local build is not accurate. So if you're testing locally and getting discouraged, just deploy it and see what happens. You might be pleasantly surprised.

My Approach (aka "Does It Feel Fast? π")
Look I'm not going to pretend I know if everything I did follows some official "best practices" handbook. A lot of what I did was based on perceived performance β does it feel fast when I use it and does it make sense? Do things load smoothly? Is there annoying layout shift? Also, this is an experiment and a hobby project, so some of the things I did were can be rightfully seen as an exeggeration, i.e. feel free to judge me π.
That said, I threw a bunch of optimisations at this thing, and collectively they worked. Here's what I did:
Images Were Eating My Performance
First thing I attacked was images. Next.js has great image optimisation built in, but you gotta configure it properly.

I set up AVIF and WebP formats (way smaller than JPEGs), and added device-specific sizing. Then I went aggressive on caching β 1 year cache headers on images. Why? Because once someone downloads an image, there's no reason to download it again.
The file size difference is real β I was seeing 30-50% smaller images with modern formats.
Again, reminder, I am not teaching, I am just sharing what I did so if someone knows better, please feel free to level some serious criticism, I'm happy to learn.
Fonts Can Block Your Entire Page
This one was really strange to me at first. I used to just throw Google Fonts in there and call it a day. But fonts can actually block your page from rendering while they load. Next.js has a solution:
The display: 'swap' is key β it shows a fallback font immediately while your web font loads in the background. No more blank screens waiting for fonts.
Cache Everything That Doesn't Change
I went a bit crazy with caching:
- Static assets: 1 year
- Fonts: 1 year
- Optimised images: 1 year
Why so aggressive? Because these files don't change. When I deploy a new version, Next.js generates new filenames anyway, so there's no risk of serving stale content. Feel free to disagree.
Code Splitting Saved My Bundle Size
This was solid. I had some heavy components β charts, modals, stuff like that. They were bloating my initial JavaScript bundle. So I dynamically imported them:
The ssr: false means it only loads on the client, and only when needed. This cut my initial bundle size by...I don't know how much but it was definitely by a lot.
Tree-Shaking Icons
Icon libraries are sneaky bundle size killers. I was using lucide-react and @radix-ui/react-icons, which have like hundreds of icons. I was only using maybe 15.
Database Queries
On the backend I made sure to only fetch the columns I actually needed, which makes perfect sense. I also used joins instead of multiple queries when I needed related data. Fewer round trips to the database = faster API responses.
Skeleton Loaders Are Your Friend π
Layout shift is annoying. You know when you're reading something and suddenly the whole page jumps because an image loaded? Not a great UX is it.
I added skeleton loaders for basically everything that loads asynchronously. I have 7 different loading.tsx files for different sections:
This keeps the layout stable while content loads.
Server Components vs Client Components
Next.js 13+ has Server Components thing and I think it's great for performance. The basic rule I follow:
- Server Components (default): Marketing pages, content that doesn't need interactivity, and of course anything that renders a lot of data
- Client Components: Forms, interactive widgets, anything that needs useState or onClick and that does not require a whole lotta data.
Here's why I think the Server Components are so good for data-heavy stuff: they render on the server, so all that data processing happens server-side. The client just gets the final HTML. No huge JSON payloads sent over the network, no client-side rendering loops.
For example, I have an analytics dashboard that displays tons of data, charts, metrics β all that gets rendered on the server. The browser just receives ready-to-display HTML. Compare that to a traditional SPA where you'd fetch all that data, parse it client-side, then render it. Way more work for the browser.
Server Components mean less JavaScript shipped to the browser. Less JavaScript = faster page.
What do you think?
The Little Things That Add Up:
- Middleware efficiency β I excluded static assets from auth checks. No point checking authentication on every single image request...or is there?
- useMemo for expensive calculations β Had a filtering function that ran on every render so wrapped in useMemo
Some Honest Thoughts
Did I need to do all this? Maybe not. How cyber safe is it? It certainly has flaws. Could some of these optimisations be overkill? Possibly. But the end result is a site that feels fast and I experimented the hell out of this thing...π most importantly I had fun.
I'm sure there are more optimisations I have missed and this is where you come in. Also at some point you gotta ship the thing and move on to actually building features and enjoy life.
Also, seriously, don't stress too much about your local Lighthouse scores. They can be insanely misleading. Test in production (or a production-like environment) to get real numbers.
My Stack
For context, I'm using:
Next.js 14 with App Router
TypeScript
Tailwind CSS
Supabase (PostgreSQL)
Deployed on Vercel
Hope someone find some value in this!
Happy coding!!! π





Top comments (2)
π nice bro
Nice!