
An audit sheet that reports 100 in every category usually looks like vendor marketing until you have to produce one yourself. On this project the acceptance criterion was agreed before the first commit: run the URL through pagespeed.web.dev and see a clean sheet with no asterisks. Delivering that took four work packages — publishing platform screening, cache and CDN topology, asset handling, and JavaScript payload removal — and the last package ended in a framework swap. What follows is the delivery log, reordered the way an engineering team would repeat it.
The written requirements were: light, fast, uncluttered, able to run several blogs from one codebase, and accessible. What nobody predicted was the cost. Removing Next.js, the framework I had used most, turned out to be the most painful step in the whole delivery.
Any conversation about blog infrastructure drags Medium, Blogger and Ghost into the room. All three were evaluated; all three failed. Medium and Blogger carried far more product surface than a text site needs, and their measured performance was disappointing.
Blogger, as captured by the audit tool:

Medium, same test, same expectations:

Those numbers ended the hosted-platform track quickly. The second blocker was workflow: staying close to the raw markup that engineers actually enjoy — Markdown, HTML, CSS — remained cumbersome there.
Every figure quoted in this article was taken with pagespeed.web.dev, so the shortlist was compared on one repeatable tool instead of impressions. The wider field was recorded like this:
| Platform evaluated | Blocking issue recorded during the trial |
|---|---|
| WordPress | Plugin hell; I did not enjoy it |
| Ghost | Self-hosting required; ghost.org felt much too expensive; editor problems after version upgrades made updates annoying |
| Hugo | Strongest of that group, but setup still required effort |
| Bear Blog | Extreme minimalism became a drawback; editor UX did not suit me; no AdSense, no free image attachments |
| Next.js (self-built) | Framework runtime JavaScript shipped to static pages kept the score below a clean 100 |
Looking back, the requirement list described something SaaS-like: as fast as Bear Blog, with a little more functionality and better usability. The build that answers it is bloffy.co. The rest of this article is how the performance was earned.
Caching is essential. Handled well, it improves performance dramatically, and Next.js made it convenient: ISR on one side, a next option that extends fetch with caching on the other. Using those two features properly already moves a site far ahead of the field.
The complication is geography. The site is reachable from anywhere, so regional visitors get fast cached responses while visitors in other regions still pay network latency. That is the job a CDN exists for. Deploying a Next.js app to Vercel hands you a CDN automatically; I do not dislike Vercel, and it is very convenient. The trade-offs are still real: the Pro plan starts at a high price, serverless cold starts remain a problem, and dependence on the Vercel ecosystem cannot be ignored.
Cloudflare was the better fit here — large-scale infrastructure plus most of the application can run on it for free. Once that layer was in place, Next.js caching stopped earning its keep: publishing one post meant invalidating two caches, and cache management became painful. For a static website, a CDN alone seemed enough. That was reason number one for not needing Next.js.
Modern images routinely arrive over 10MB, and pushing those files to the web unchanged destroys performance. The work behind a single image is also tedious: resized variants per device, compression, and then another CDN to manage. On an earlier project that whole pipeline was hand-built — a serverless function on AWS Lambda compressing and resizing on arrival in storage on S3, with CloudFront in front as the CDN. Full control, but it needed regular maintenance.
For this blog the choice was Cloudflare’s Image Transformations. Even the free tier includes 5,000 images, so it gets used without hesitation. At this point I may sound like a Cloudflare sales rep, but I am only an ordinary solo developer.
With caching and images handled, the Next.js build produced this score sheet.

For reference, this was Next.js 16.1.6. The scores were pretty high, but not enough: I wanted a clean 100, with no asterisks, so the remaining audit items had to be chased down.

The report flagged a substantial amount of legacy JavaScript, and none of it was code I wrote. It came from the framework runtime that Next.js ships in its bundle. The frustrating part was that the page was static with zero interaction, every page was pre-rendered with ISR, and the official Next.js guidelines had, as far as I could tell, been followed properly.
So the experiment was: remove every client component in the app, leave only server components, measure again. The score did not budge.
Hydration was the first suspect, and it was not the whole story. The real cause is the client runtime Next.js always ships, even for static pages. With server components only, the runtime chunks, hydration, and the RSC payload for the router all remain, because next/link and next/image are themselves client components ('use client') — those two alone are enough to pull the runtime in and trigger hydration.
Strip out next/link and next/image as well, and the App Router's own runtime — the router, the error boundaries — is still there. There is no official way to reduce it to zero; the only option was a hack: pulling the scripts out of the rendered HTML. That is when it hit me: “If I’m going to do that, why am I even using Next.js?”
Then there was the unused code.

The bulk of the JavaScript was code that was not needed. Unused code alone came to 230,460 bytes. That is not good. Most static websites do not need JavaScript at all, and unused JavaScript is needed even less.
This was the genuine agonising point: was dropping Next.js for the sake of 5 points the right call, after all the hours poured into the public-facing web app?
In the end, quality won: even if the gap was only 5 points, Next.js came out entirely, because the framework's excess features would not be needed in the long run either. Going fully vanilla was considered and rejected — the productivity and infrastructure benefits of a framework were still wanted. The right fit had to be found.
The choice was Hono, because of its technical philosophy. The phrase “Fast and simple” was exactly the solution needed most, JSX support came as a bonus, and the migration cost did not look too big.
Finally, 100.

Just like that, the blog reached 100 across the board — and the page you are reading now can be run through pagespeed.web.dev as a check.

There is no JavaScript on the page, and unused code dropped sharply to 407 bytes. As a bonus, build time went down by about 40%. Two notes on that screenshot: the SEO score of 69 belongs to a site built for testing that deliberately blocks search indexing — that is why it reads 69. And if you score 100 on everything in DevTools, Lighthouse actually sets off fireworks for you.
| Next.js Performance (server components only) | Hono Performance |
|---|---|
![]() | ![]() |
The long task that runs before anything is rendered on screen took 309.3ms on Next.js. Building the same page with Hono took 68.01ms — roughly a 78% improvement on that single metric, which is what the last few audit points were actually made of.
Is a JS payload, hydration included, really doing a static site any good? I do not think so. It leaves behind unnecessary JavaScript, so in most cases I think it is simply not needed.
No. For an interaction-heavy app, Next.js can be a great framework — the management app for this very blog is built with Next.js.
It is convenient and hands you a CDN automatically, but the Pro plan starts at a high price, serverless cold starts remain a problem, and ecosystem dependence cannot be ignored.
Because publishing a single post requires invalidating both. For a static website, the CDN alone looked sufficient.
That particular site was built for testing and deliberately blocks search indexing; that is why it scores 69.
Framework choice is a delivery decision, not an identity. In this build the framework that had carried the most previous work was the one thing standing between a high score and a full one, and the audit trail above is repeatable: measure on one tool, remove the two easy bottlenecks, read the leftover JavaScript items as a runtime signal, and only then decide whether the migration is worth the last few points. On this site it was, and the check is one URL and one click away in pagespeed.web.dev.