All WordPress HTML Templates Forms & Webhooks AI & Tools
WordPress

The 2026 WordPress Performance Playbook: From TTFB to Core Web Vitals

A practitioner’s guide to WordPress performance optimization in 2026: fixing TTFB, autoloaded options, object caching, LCP, INP and speculative loading.

A real working moment illustrating the theme of an article about wordpress performance optimization. Wide 16:9 banner, one strong focal point, magazine editorial quality, authentic and unstaged.

A client shipped us a WordPress site with a 96 Lighthouse score on desktop and a Search Console report showing 41% of real mobile URLs failing Interaction to Next Paint. Both numbers were correct. The lab test ran on a simulated Moto G4 with no cookie banner, no logged-in state, no chat widget and no ad script, because none of those load when you hit the page cold from a datacenter in Iowa.

That gap is the whole story of WordPress performance optimization in 2026. The easy wins have been absorbed into core: lazy loading, fetchpriority on the LCP image, AVIF support, speculative loading. What’s left is the stuff no plugin will do for you, and most of it lives either in your database or in the third-party JavaScript your marketing team added last quarter.

Below is the sequence we work through on client sites, in order, with the measurements we take at each step.

Key Takeaways

  • Field data (CrUX, 75th percentile, 28 day rolling window) is the only thing Google ranks on. A Lighthouse score is a debugging tool, not a target.
  • TTFB above roughly 600 ms on a cached page is a hosting or edge problem. TTFB above 600 ms on an uncached page is almost always autoloaded options or an unindexed wp_postmeta query.
  • INP is where WordPress sites fail now. The usual suspects are jQuery-dependent sliders, cookie consent scripts and chat widgets, not your theme’s CSS.
  • WordPress 6.8 ships the Speculation Rules API by default. Switching it from conservative prefetch to moderate prerender is a two line filter and often the single largest perceived speed change you’ll make.
  • Every performance plugin you add is another set of hooks running on every request. On several audits we’ve removed the optimisation plugin and gone faster.

Measure the thing Google actually measures

Core Web Vitals assessment uses the Chrome User Experience Report: real Chrome users, 28 day rolling window, 75th percentile per metric, split by mobile and desktop. Your thresholds are LCP at 2.5 seconds, INP at 200 ms and CLS at 0.1. Passing means all three at the 75th percentile, which means one in four visitors can still be having a bad time while you’re “green”.

Get three data sources running before you change anything:

  • PageSpeed Insights or the CrUX API for origin-level and URL-level field data. Origin-level matters because low-traffic URLs inherit it.
  • A real user monitoring script. The web-vitals library (v4+) with the attribution build tells you which element caused the LCP and which script blocked the interaction. Ship it to your own endpoint. Roughly 1.5 KB gzipped for the attribution build, and it will save you a week of guessing.
  • Query Monitor in staging, logged in as an admin, with SAVEQUERIES on. It’s ugly and it’s the best debugging tool WordPress has.

The attribution data is the part people skip. Knowing your INP is 480 ms is useless. Knowing that 380 ms of it is script evaluation in cookie-consent.js during the pointerdown handler on the mobile menu is a fix you can ship this afternoon.

TTFB: the server half of the problem

Google doesn’t rank on TTFB directly, but LCP can’t be faster than TTFB plus the time to fetch and paint the hero. If your TTFB is 900 ms you have 1.6 seconds left for everything else, and on a mid-range Android over 4G that’s not enough.

Split the diagnosis in two. Hit a page that is definitely in your page cache, then hit one that isn’t (add a query string, or check a logged-in view). If cached TTFB is fine and uncached is 1.2 seconds, PHP and MySQL are your problem. If both are slow, it’s the network path or the host.

Autoloaded options

Every WordPress request loads all autoloaded options into memory before it does anything else. WordPress 6.6 added a guard that stops single options over about 150 KB from being autoloaded, plus new autoload values (on, off, auto, auto-on, auto-off), but it doesn’t retroactively fix the 400 small rows a decade of plugins left behind.

-- Total autoloaded payload. Under 200 KB is healthy. Over 800 KB, you have work to do.
SELECT ROUND(SUM(LENGTH(optionvalue)) / 1024, 1) AS autoloadkb
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto', 'auto-on');

-- The worst offenders, by name
SELECT optionname, ROUND(LENGTH(optionvalue) / 1024, 1) AS size_kb
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto', 'auto-on')
ORDER BY LENGTH(option_value) DESC
LIMIT 25;

We audited a WooCommerce store last year with 3.1 MB of autoloaded options, most of it orphaned transients and a licensing plugin storing serialised API responses. Setting the dead rows to off took uncached TTFB from 1.4 s to 610 ms. That was the entire fix. No caching plugin involved.

PHP and OPcache

Run PHP 8.3 or 8.4. The jump from 7.4 was worth roughly 20 to 25% on request time in our benchmarks; the jump from 8.1 to 8.3 is smaller but free. Turn JIT off. It does nothing measurable for WordPress workloads and adds memory pressure.

# php.ini values that matter for a busy WP site
opcache.enable=1
opcache.memory_consumption=256
opcache.maxacceleratedfiles=20000   ; WP core alone is ~7,000 files
opcache.validate_timestamps=0          ; production only: you MUST reset OPcache on deploy
opcache.jit=disable

If you set validatetimestamps=0 and your deploy script doesn’t call opcachereset() or restart PHP-FPM, you will ship code that doesn’t run. Ask us how we know.

The caching stack that holds up

Four layers, in order of how much they save you:

  1. Full page cache at the edge. A cached HTML response served from a POP 40 ms away beats every PHP optimisation combined. Cloudflare, Bunny, Fastly, or your host’s built-in edge cache.
  2. Full page cache on origin. Nginx FastCGI cache or a plugin writing static HTML. Catches anything the edge misses.
  3. Persistent object cache. Redis or Memcached. This is the layer that helps logged-in users, WooCommerce carts and the admin, which is to say all the traffic your page cache can’t touch.
  4. Transients and fragment caching for your own expensive queries.

Object caching is the layer most sites skip. Without it, every get_option() miss, every term lookup and every meta query hits MySQL on every request. With Redis on the same host, a WooCommerce category page in our testing dropped from about 340 queries to 60.

// wp-config.php, above the "That's all, stop editing" line
define( 'WPREDISHOST', '127.0.0.1' );
define( 'WPREDISPORT', 6379 );
define( 'WPREDISPREFIX', 'clientsite_' ); // critical when several sites share one Redis instance
define( 'WPREDISMAXTTL', 86400 );
define( 'WPREDISIGBINARY', true );        // needs ext-igbinary; ~30% smaller payloads than serialize
define( 'WPCACHEKEYSALT', 'clientsite' );

One caveat: an object cache with no eviction policy and a full maxmemory will start returning misses silently, and your site gets slower than it was without the cache at all. Set maxmemory-policy allkeys-lru in redis.conf and monitor the hit rate. Below 90% means something is writing cache keys it shouldn’t.

LCP is nearly always one image

Find the LCP element. It’s a hero image, a background image on a section, or an H1 sitting under a webfont that hasn’t loaded. Then work through this list:

  • Never lazy load it. WordPress has handled this automatically since 6.3, adding fetchpriority="high" and skipping loading="lazy" on the first large in-viewport image. It gets this wrong when your hero is a CSS background or comes from a page builder. Check the rendered HTML rather than trusting it.
  • Serve AVIF. Core has supported it since 6.5. Our typical hero drops from 180 KB WebP to about 95 KB AVIF at visually identical quality.
  • Preload the image, not the font. If the LCP element is an image, a font preload competes for the same bandwidth during the critical window.
  • Kill render-blocking CSS above 40 KB. Inline the critical rules, load the rest with media="print" onload or a rel=preload swap.

For CSS background heroes there’s no automatic help, so do it manually:

addaction( 'wphead', function () {
    if ( ! isfrontpage() ) {
        return;
    }
    // imagesrcset/imagesizes must match the CSS image-set() or the browser fetches twice
    echo '<link rel="preload" as="image" href="/hero-1600.avif" fetchpriority="high">';
}, 1 );

A preload is a promise. Preload something the browser doesn’t end up using and you’ve spent bandwidth in the exact window where you had none to spare. Preload one resource per page. Two at most.

INP is where WordPress sites lose

INP replaced FID in March 2024 and it’s a far harder metric. FID measured input delay only. INP measures the whole interaction: delay, the event handler running, and the next paint. A 200 ms budget covers all three, on the slowest interactions across the entire visit.

In our audits the causes cluster tightly:

  • Cookie consent banners. Some of these ship 200 KB of JavaScript and re-scan the DOM on every click. This is the single most common INP killer we find.
  • Chat widgets. Intercom, Drift, Tawk. Load them on interaction, not on page load. A button that swaps in the real widget on first click costs you 300 ms once, for the 2% of users who click it.
  • jQuery-based sliders and mega menus. A menu that runs a delegated jQuery handler across 200 nodes on mouseover will blow the budget on a mid-range phone.
  • Layout thrashing in theme JS. Reading offsetHeight inside a loop that also writes styles forces synchronous reflow per iteration.

When you do have work that must run in a handler, yield back to the browser so it can paint:

async function handleFilterClick(event) {
  updateButtonState(event.target);        // cheap: paint this immediately

  // scheduler.yield() ships in Chrome 129+; fall back to a task-scheduling setTimeout
  if ('scheduler' in window && 'yield' in scheduler) {
    await scheduler.yield();
  } else {
    await new Promise((r) => setTimeout(r, 0));
  }

  renderFilteredResults();               // expensive: now runs after the paint
}

That one change took a product filter from 410 ms INP to 90 ms on a client’s catalogue. The work still takes the same total time. The user just sees the button react first, which is the entire point of the metric.

On the CSS side, modern selectors let you delete a lot of the JavaScript that was only ever there to toggle classes. We covered the specifics in CSS nesting :has() and the features that changed how we write CSS, and :has() in particular has replaced whole menu scripts on sites we maintain.

Speculative loading: the free win most sites haven’t switched on

WordPress 6.8 shipped the Speculation Rules API in core. The default is prefetch with conservative eagerness, which only triggers on mousedown. Safe, and barely noticeable. Moving to prerender with moderate eagerness (triggered after about 200 ms of hover) makes the next navigation feel instant, because the page is already rendered.

addfilter( 'wpspeculationrulesconfiguration', function ( $config ) {
    return array(
        'mode'      => 'prerender',
        'eagerness' => 'moderate',
    );
} );

Do not do this on a WooCommerce store without excluding cart, checkout and account URLs. Do not do it if your analytics or A/B testing scripts fire on load without checking document.prerendering. You’ll inflate pageviews and, worse, fire add-to-cart side effects on pages nobody visited. Core already excludes common WooCommerce endpoints and anything with a nonce in the URL, but audit your own custom endpoints. Prerender also costs the user memory and data, so on a content site with 30 links per page, moderate eagerness is the sensible ceiling.

Cut the stack before you optimise it

The uncomfortable finding from about a dozen audits: on roughly a third of them, deactivating the all-in-one optimisation plugin made the site faster. Those plugins hook template_redirect, buffer the entire output, run regex over the HTML, and add their own database tables and cron jobs. When the host already does page caching at the edge, you’re paying for the same work twice.

What we actually do instead:

  • Audit plugins by request cost, not by count. Query Monitor’s plugin timing panel will tell you that one booking plugin costs 240 ms per request while fifteen others cost 4 ms combined.
  • Dequeue assets per template. A contact form’s CSS and JS has no business on 200 blog posts.
  • Disable WP-Cron on the request path with define( 'DISABLEWPCRON', true ) and run it from system cron every minute. Otherwise an unlucky visitor pays for your scheduled tasks.
  • Throttle the Heartbeat API in the admin. On editorial sites with ten people in wp-admin, admin-ajax.php can be the busiest endpoint on the box.
addaction( 'wpenqueue_scripts', function () {
    if ( isadmin() || ispage( 'contact' ) ) {
        return;
    }
    wpdequeuestyle( 'contact-form-7' );
    wpdequeuescript( 'contact-form-7' );
}, 100 ); // priority 100: run after plugins have enqueued

For forms specifically, the cheapest option on a mostly-static site is not to run a form plugin at all. A plain HTML form posting to an endpoint like WebForms removes the plugin, its database tables and its front-end JavaScript in one go.

The same logic applies to how the site is built in the first place. Themes that generate clean, predictable markup give you a much shorter performance to-do list than a builder emitting eight nested divs per section. If you’re specifying a new WordPress build, CanvasWP and a curated block pattern library will get you further than any optimisation plugin bolted on afterwards.

Frequently Asked Questions

How long after fixing an issue will Core Web Vitals update?

CrUX uses a 28 day rolling window, so expect partial movement after about a week and the full effect after four weeks. PageSpeed Insights shows you the same 28 day data. If you need faster feedback, use your own RUM data, which updates in real time and lets you compare before and after by deploy date.

Is a Lighthouse score of 100 worth chasing?

No. Lighthouse runs a synthetic test with a fixed CPU throttle and no third-party scripts blocked by consent state, so it systematically underreports INP and overreports how well you’re doing. Use it to find specific problems (unused CSS, oversized images, render-blocking resources), then verify the fix in field data.

Do I still need a caching plugin if my host has server-level caching?

Usually not for page caching, and running both often makes things worse through double buffering and conflicting cache-clear logic. What you may still need is a persistent object cache drop-in, which most managed hosts either provide or let you enable. Check what your host actually does before installing anything.

What’s a realistic TTFB target for WordPress?

Under 200 ms for edge-cached HTML, under 500 ms for an origin-cached hit, and under 800 ms for a fully dynamic uncached request like a logged-in cart page. If dynamic requests exceed a second, look at autoloaded options and slow queries before you upgrade the server, because vertical scaling rarely fixes a missing index.

Does WooCommerce make passing Core Web Vitals impossible?

It makes it harder, not impossible. Cart fragments over admin-ajax and the sheer volume of scripts on product pages are the main obstacles. Disable cart fragments where you don’t need a live cart count, enable High-Performance Order Storage, and load payment provider scripts only on checkout. We’ve taken stores with 500 products to passing on all three metrics without rebuilding the theme.

Where to start on Monday

Run the autoload query first. It takes thirty seconds and on maybe half the sites we look at it’s the biggest single win available. Then get attribution-level RUM data in place so your next decision is based on what your users experience rather than what a datacenter in Iowa experiences.

After that, the order is fixed: TTFB, then LCP, then INP, then CLS. Fixing LCP before TTFB is like tuning the suspension on a car with a flat tyre. And if your audit ends with “remove three plugins” rather than “install a new one”, you’ve probably got it right.

autoloaded options wordpress slow how to fix INP in wordpress redis object cache wordpress config reduce wordpress TTFB wordpress core web vitals field data wordpress performance optimization wordpress performance optimization 2026 wordpress speculation rules prerender