When Hraparak.am asked me to look at their performance problems, the site was crashing during exactly the moments it was most needed. Major political events, breaking news — when traffic spiked, the site went dark. That's a business problem as much as a technical one. A news site that goes offline during breaking news stops being a news site.
The brief was to handle 10,000+ concurrent users without going down. Here's what actually fixed it.
Measure first, fix second
Before touching any code, I ran a full audit. Not guessing at the problems — actually measuring them. The numbers:
- Total Blocking Time: 1,200ms
- Largest Contentful Paint: 4.8 seconds
- Cumulative Layout Shift: 0.31
- Average server response time: 800ms — every page hit the database fresh, no caching at all
Four numbers, four problems. The fixes followed directly from the measurements. This is worth saying because the temptation on performance work is to start with what you already know how to fix. The actual bottleneck is often somewhere you didn't expect.
The biggest win: stop asking the database
Every article page was server-rendered on every request. Every visitor triggered a database query. During a traffic spike, the database was drowning and taking the whole site with it.
Switching to ISR (Incremental Static Regeneration) cut server response time from 800ms to 28ms. Article pages now live on the CDN edge — when someone reads an article, the request never even reaches the origin server. The page regenerates every 60 seconds if the content has changed, and gets served statically everywhere else.
This single change handled most of the traffic problem. The origin went from getting every request to getting almost none.
The JavaScript bundle was eating the main thread
680KB of JavaScript, uncompressed. Most of it was components article pages don't need at all — the homepage carousel, the comment widget, the advertising manager. All of it blocking the main thread while users waited to read the article.
Dynamic imports fix this: split off the components that aren't needed immediately, load them only when they enter the viewport. TBT dropped from 1,200ms to 180ms after this change alone.
Images were the main LCP problem
Every article had a hero image. Editors uploaded whatever they had — often uncompressed JPEGs, regularly 3–5MB. The server served them as-is. An image displaying at 800px wide doesn't need to be 4MB.
Replacing standard img tags with the Next.js Image component fixed this automatically: WebP conversion on supporting browsers, resizing to the actual render size, lazy loading for images below the fold. Hero image payload dropped from 3.2MB to 180KB. LCP improved from 4.8 seconds to 1.4 seconds. Best effort-to-impact ratio in the whole project.
Fonts were causing layout shift
Three custom fonts, loaded as CSS imports — a render-blocking pattern that delays First Contentful Paint until all fonts are downloaded. When they finally load, text jumps, causing layout shift and a jarring experience on slower connections.
next/font with display: swap and preloading the primary face fixed it. CLS went from 0.31 to 0.04.
The real test
Three weeks after the fixes went live, a major political event drove a large traffic spike. The site handled 12,000 concurrent users. Average load time: 1.1 seconds. Zero downtime.
The previous comparable spike — smaller, at 4,000 concurrent users — had caused 40 minutes of total outage.
The actual lesson
None of this required new infrastructure. No new servers, no CDN contract renegotiations, no database upgrade. All of it was using what was already there more correctly.
ISR, bundle splitting, image optimisation, and font loading are all expressions of the same idea: do the work once, as early as possible, and serve the result rather than repeating the work on every request. Most performance problems I've seen on Next.js apps come down to ignoring this principle somewhere obvious.
If your site is slow, measure first. The bottleneck is almost never where you think it is.
If you're building or maintaining a Next.js application and running into performance issues, describe the problem — I can usually tell you within a day where to look.