Most comparisons of headless and traditional WordPress are written from the outside: a list of pros, a list of cons, and a conclusion that it depends. I wanted numbers, and I happened to have the perfect test subject. This site is headless. The content lives in WordPress, and what you’re reading was rendered by Next.js and served from Netlify.

So I measured it. What I found was more useful than a clean win for headless, because one of my own pages turned out to be slower than it should have been.

The setup

Two pieces make up this site:

  • WordPress on ordinary Apache hosting. It stores posts, case studies and form submissions, and answers requests through its REST API.
  • Next.js on Netlify. It fetches content from WordPress, renders the pages and serves them from Netlify’s CDN.

The WordPress front end no longer shows anything to visitors. A small plugin sends every front-end request to the matching page here with a 301 redirect. That gave me a neat way to measure WordPress itself: the redirect fires on template_redirect, after WordPress has loaded, connected to the database and run the main query for the post, but before a theme renders anything. The time to that redirect is the minimum a traditional theme would need before it sends a single byte of HTML.

How I measured

From my desk in India, I made 30 requests to each URL with curl and recorded two timings:

  • TTFB, the time to first byte, including DNS, the TCP connection and TLS.
  • Server wait, the time between the secure connection being ready and the first byte arriving. That’s the server’s thinking time plus at least one network round trip.

Then I loaded the pages in headless Chrome, throttled to the profile Lighthouse uses for mobile (150 ms latency, 1.6 Mbps download, a CPU four times slower), and took the median of three runs.

Result 1: a cached page beats WordPress on two counts

RequestTTFB medianServer wait medianServer wait p90
WordPress REST API, one post1,543 ms963 ms1,017 ms
WordPress front end, same post (to the redirect)1,557 ms968 ms1,089 ms
A static CSS file on the same WordPress servern/a604 msn/a
Next.js homepage, served from cache358 ms79 ms573 ms
Next.js blog post, rendered per request781 ms596 ms762 ms
30 requests per URL (15 for the static file). Server wait runs from the secure connection being ready to the first byte, so it includes at least one network round trip.

That last point matters, because the two servers aren’t the same distance from me. A TCP connection, which takes one round trip, took 276 ms to the WordPress host and 70 ms to Netlify’s nearest edge. To separate distance from processing, I also timed a plain CSS file from the WordPress server. Apache sent it without running any PHP, and it still took 604 ms.

That gives two separate costs:

  • WordPress’s own work: about 360 ms on top of the static file, to boot, query the database and decide what to do. The REST API and the front end take the same time, so a theme would add its rendering time on top.
  • Distance: a CDN serves cached pages from a location near the visitor. My WordPress host can only answer from where it is.

The cached Next.js homepage benefits from both. Its 79 ms of server wait is barely more than the 70 ms round trip, so Netlify spent almost no time on it. It sent a page it had already built, from nearby.

To be fair to WordPress, a page-cache plugin plus a CDN in front of a traditional theme can achieve much of the same. The advantage comes from caching and distance. Headless is one way to get them.

Result 2: my blog posts weren’t cached at all

The last row bothered me. Blog posts were taking 596 ms of server time, and their response headers said cache-control: private, no-cache, no-store. Every visit rendered the page again.

The cause was one missing function. In the Next.js App Router, a dynamic route such as /blog/[slug] without generateStaticParams is rendered on every request. The Next.js documentation says so plainly: “You must always return an array from generateStaticParams, even if it’s empty. Otherwise, the route will be dynamically rendered.”

The fix is three lines:

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  return [];
}

An empty array means nothing is built in advance, so a WordPress outage can’t break a deploy. Each post renders on its first visit and is then cached and refreshed in the background, like the rest of the site. The case studies had the same problem and got the same fix.

I’m sharing this because it’s the most common way headless sites lose their speed advantage. Headless doesn’t make a site fast on its own. Caching does. If pages are rendered on every request, you’ve added a second server between the visitor and WordPress, and the result can be slower than a plain WordPress theme with a good page cache.

Result 3: what a visitor on a phone experiences

Page (throttled mobile)TTFBLargest Contentful PaintRequestsTransferredJavaScript
Homepage422 ms1,896 ms24680 KB196 KB
Blog post (before the fix)959 ms1,796 ms21334 KB196 KB
Headless Chrome, Lighthouse mobile throttling, median of three runs, browser cache disabled.

Both pages paint their main content in under 2.5 seconds, the threshold Google treats as “good” for Largest Contentful Paint. Even uncached, the post did well because the page is light: 21 requests and about a third of a megabyte.

The JavaScript figure is the honest cost of this setup. 196 KB of compressed JavaScript is the React and Next.js runtime plus the animation library. A simple WordPress theme might send far less. Headless isn’t automatically lighter. You trade server time for browser work, and you have to keep an eye on both.

What headless costs you that a benchmark won’t show

Speed is the easy part to measure. The rest is about how much there is to look after.

  1. Two systems to maintain. WordPress still needs updates, backups and security care. Next.js adds dependencies, builds and a hosting account.
  2. Plugins that touch the front end stop working. Page builders, SEO plugins that print meta tags, form plugins that render their own markup: on a headless site, all of that has to be rebuilt or fetched by hand. On this site I wrote five small WordPress plugins to provide clean API endpoints, lock the API down, store form submissions and redirect the old front end.
  3. Previews need work. “Preview” in WordPress shows the WordPress theme, not your Next.js page, unless you build a preview route.
  4. More moving parts can fail. When WordPress is down, a well-built headless site keeps serving cached pages, which is a real advantage. A badly built one fails its next build or shows errors.

When I’d recommend each

SituationMy recommendation
A brochure site that editors update weekly, on a tight budgetTraditional WordPress with a lightweight theme and a page cache
A content-heavy site where page speed and SEO are the main goalsHeadless, if someone will look after the caching
A site that is really an application (dashboards, logins, tools)Headless. WordPress stays the content store; Next.js does the app.
A team that relies on a page builder like ElementorTraditional. Going headless throws most of that work away.
Content shared across a website, an app and other channelsHeadless. One API serves them all.

What I took from this

The numbers backed up the reason I went headless: a cached page served from a nearby CDN answered in 79 ms of server wait, against nearly a second for WordPress building the same content on demand. They also showed how easily that advantage disappears. My own blog posts had been rendered on every visit, and I only noticed because I measured.

If you’re thinking about going headless, start by measuring what you have. Time your current server wait, check your cache headers, and look at the JavaScript you send. Those three numbers will tell you more than any list of pros and cons.