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
| Request | TTFB median | Server wait median | Server wait p90 |
|---|---|---|---|
| WordPress REST API, one post | 1,543 ms | 963 ms | 1,017 ms |
| WordPress front end, same post (to the redirect) | 1,557 ms | 968 ms | 1,089 ms |
| A static CSS file on the same WordPress server | n/a | 604 ms | n/a |
| Next.js homepage, served from cache | 358 ms | 79 ms | 573 ms |
| Next.js blog post, rendered per request | 781 ms | 596 ms | 762 ms |
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) | TTFB | Largest Contentful Paint | Requests | Transferred | JavaScript |
|---|---|---|---|---|---|
| Homepage | 422 ms | 1,896 ms | 24 | 680 KB | 196 KB |
| Blog post (before the fix) | 959 ms | 1,796 ms | 21 | 334 KB | 196 KB |
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.
- Two systems to maintain. WordPress still needs updates, backups and security care. Next.js adds dependencies, builds and a hosting account.
- 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.
- Previews need work. “Preview” in WordPress shows the WordPress theme, not your Next.js page, unless you build a preview route.
- 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
| Situation | My recommendation |
|---|---|
| A brochure site that editors update weekly, on a tight budget | Traditional WordPress with a lightweight theme and a page cache |
| A content-heavy site where page speed and SEO are the main goals | Headless, 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 Elementor | Traditional. Going headless throws most of that work away. |
| Content shared across a website, an app and other channels | Headless. 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.