2 sites for $49.99 ·

AcceleratorWP
All posts
Performance

How to Speed Up WordPress Without a Cache Plugin (2026)

Nine structural ways to make WordPress faster without WP Rocket, LiteSpeed, or any cache layer — what actually moves TTFB on the requests cache can't touch.

Sarp EfeMay 10, 202612 min read

Every WordPress speed tutorial starts the same way: install a cache plugin. It's the default advice because for blog-style content sites, it's mostly right. A static HTML response served from disk will always beat a PHP request that has to boot WordPress, query the database, run filters, render templates, and emit HTML.

But there's a problem with treating caching as the answer: it doesn't actually make WordPress faster. It lets WordPress not run on most requests. The moment you have logged-in users, a cart, a checkout, a form submission, a REST call, an admin AJAX request — anything that bypasses cache — you're back to running the full PHP stack, just as slow as before.

This post is about the other half. The half nobody writes about. Making WordPress fast for the requests cache can't help.

I run AcceleratorWP, a plugin that does some of these things by default. I'll mention it at the very end. The rest of this post stands alone — these are techniques you can apply without installing anything I sell.

Why "without cache" is even a goal

Three audiences specifically need this:

  1. WooCommerce store owners where the cart, checkout, and account pages serve every shopper individually and can never be cached.
  2. Membership and LMS sites where 60% of traffic is logged-in users hitting personalized dashboards.
  3. Anyone debugging slow admin — your wp-admin is logged-in, dynamic, and impossible to cache. Yet you live in it every day.

If any of that is you, "just install WP Rocket" is not the answer. The answer is to make the underlying WordPress request cheaper. Here's how.

A quick TTFB reality check before we start

Open Chrome DevTools on a page that isn't cached on your site — your wp-admin dashboard works. Network tab, click the document request, read "Time to First Byte" under Timing.

  • Under 200ms: you don't have a problem.
  • 200-500ms: there's something to fix but it's not urgent.
  • 500-1500ms: this post is for you.
  • Over 1500ms: this post is especially for you.

For reference: a fresh WordPress 6.x with no extra plugins typically does 100-200ms on cheap shared hosting. Every plugin you add usually piles on 10-50ms because WordPress loads them all on every single request — including requests that don't need them.

That's the core problem caching can't solve. Let's hit it directly.

Method 1: Per-page plugin loading

WordPress loads every active plugin on every request. The About page loads WooCommerce. The blog archive loads your Bookings plugin. A REST API call to fetch posts loads your CSS animation library and your contact form. None of these plugins contribute anything to those requests, but they all consume PHP cycles, register hooks, query the database for their settings, and ship CSS/JS to the browser.

The fix is structural: load plugins selectively based on what the current request actually needs.

Implementing it by hand: write a mu-plugin (a "must-use" plugin in wp-content/mu-plugins/) that hooks into option_active_plugins and filters out plugins based on URL pattern. The filter runs before WordPress decides which plugins to boot, so a removed plugin literally doesn't load. Code is short:

<?php
add_filter('option_active_plugins', function ($plugins) {
    $url = $_SERVER['REQUEST_URI'] ?? '';
    if (str_starts_with($url, '/about')) {
        $plugins = array_values(array_filter(
            $plugins,
            fn ($p) => !str_contains($p, 'woocommerce')
        ));
    }
    return $plugins;
});

That's it. WooCommerce no longer loads on /about. Measured impact on a typical WC site: 20-40ms saved per request, 6-10 fewer CSS/JS files in the response. The Network tab tells the story.

The catch: hand-writing these rules for every plugin × every page pattern × every request type doesn't scale. You'll write 30 rules, miss 50, and accidentally skip a plugin on a page that needed it. Either you build tooling for this (a rule engine, page fingerprinting, a soak window for testing) or you use something that already did. Five hand-written rules for the most obvious skips is genuinely valuable and free. Start there.

Method 2: Hook pruning

WordPress's plugin architecture is built on hooks. Every plugin attaches functions to actions and filters, and WordPress fires those hooks at specific points during request processing. The default is generous: most plugins attach to dozens of hooks on init, wp_loaded, template_redirect, wp_enqueue_scripts, and so on, regardless of whether the current request will use any of them.

You can remove hooks selectively. A WooCommerce filter that adds cart-fragment markup to every page can be detached on pages where no cart icon is rendered. A Yoast filter injecting schema markup can be removed on REST API requests. The mechanism is remove_action and remove_filter, called early enough to take effect:

add_action('plugins_loaded', function () {
    if (defined('REST_REQUEST') && REST_REQUEST) {
        remove_action('wp_head', 'wpseo_head');
    }
}, 1);

How much it helps: depends entirely on which hooks you're pruning. A heavy WooCommerce hook that runs a database query for cart state can save you 30-80ms when removed on a page that has no cart. A Yoast hook that only adds inline output saves you a millisecond and isn't worth the maintenance.

The skill here is knowing which hooks are expensive. Query Monitor (free plugin, dev-environment only) shows the per-hook timing breakdown. Look at hooks taking more than 5ms; those are your candidates.

Method 3: Per-page asset dequeue

This is the most visible win for visitors and the easiest to measure: stop loading CSS and JS files the current page doesn't need.

WordPress and plugins enqueue assets indiscriminately. Contact Form 7 loads its CSS on every page even if there's no form. Elementor Pro loads multiple JS bundles on a page rendered in the classic editor. WooCommerce ships a CSS file on every request including pages with no shop elements.

The mechanism: wp_dequeue_style and wp_dequeue_script, called on wp_enqueue_scripts priority 100 or higher (after plugins have already enqueued):

add_action('wp_enqueue_scripts', function () {
    if (!is_singular('page')) return;
    $content = get_post()->post_content ?? '';
    if (!has_shortcode($content, 'contact-form-7')) {
        wp_dequeue_style('contact-form-7');
        wp_dequeue_script('contact-form-7');
    }
}, 100);

Measured impact: on plugin-heavy sites, dequeuing 5-10 stylesheets and 3-5 scripts per page cuts transferred bytes by 100-500 KB and shaves 200-800ms off LCP on mobile connections. That's the kind of number that moves PageSpeed Insights.

Perfmatters and Asset CleanUp Pro ship a UI for this and a per-URL rule engine. You can hand-write the rules for the 5-10 most expensive assets without paying for anything.

Method 4: Throttle the heartbeat

WordPress's "heartbeat" is a JS request that fires from the admin every 15 seconds and from the frontend every 60. It supports autosave, post-locking, and admin notifications. On a site with a dozen open admin tabs, it can be doing 100+ AJAX requests an hour to your server, each one booting full WordPress to respond.

Most sites don't need the frontend heartbeat at all. The admin heartbeat is useful in the post editor but not on dashboard listing screens.

add_action('init', function () {
    if (!is_admin()) {
        wp_deregister_script('heartbeat');
    }
});
 
add_filter('heartbeat_settings', function ($settings) {
    $settings['interval'] = 60; // up from 15
    return $settings;
});

Measured impact on server load: noticeable, especially on small VPS hosts where admin traffic competes with frontend traffic for PHP workers. Worth doing on every site. Takes two minutes.

Method 5: Kill REST API user enumeration

WordPress's REST API exposes wp-json/wp/v2/users by default, which lets unauthenticated requests list every author on the site. Bots scrape this constantly to build username dictionaries for brute-force attacks. It's both a security issue and a load issue — every scraped request boots full WordPress to respond.

add_filter('rest_endpoints', function ($endpoints) {
    unset($endpoints['/wp/v2/users']);
    unset($endpoints['/wp/v2/users/(?P<id>[\d]+)']);
    return $endpoints;
});

This isn't strictly speed work — the speedup is incidental (fewer bot requests to handle) — but it's a thirty-second fix that cuts noise. Free win.

Method 6: Query memoization within a single request

WordPress doesn't deduplicate identical database queries within a single request by default. If three different plugins each ask "is the current user logged in?", you'll see three separate queries to the users table.

The fix is in-memory memoization for the duration of one request. You wrap your get_user_by, get_option, get_post_meta calls in a static cache:

function accel_get_option_cached(string $name, $default = false) {
    static $cache = [];
    if (!array_key_exists($name, $cache)) {
        $cache[$name] = get_option($name, $default);
    }
    return $cache[$name];
}

This is complex to implement safely (you can introduce subtle bugs where cached results go stale during the same request, especially around update_option calls). Most sites won't do this by hand. But the impact is real: WP Engine, Kinsta, and other managed hosts implement variants of this in their hosting layer for exactly this reason.

If you want to see the gain without writing the code, install Query Monitor and look at the "Duplicate Queries" panel. Every line there is wasted work.

Method 7: Autoload optimization

WordPress's wp_options table has an autoload column. Options with autoload = yes are loaded into memory on every single request, regardless of whether anything uses them.

Over time, plugins add autoloaded options they no longer need but never clean up. Deactivated plugins leave behind hundreds of KB of autoload payload. A five-year-old WordPress site easily carries 5-10 MB of autoloaded options.

Fix: query wp_options for the largest autoloaded rows, manually flip the ones you recognise as cruft to autoload = no. WP-Optimize has a tool for this. Or run the query yourself:

SELECT option_name, LENGTH(option_value) AS size
FROM wp_options
WHERE autoload = 'yes'
ORDER BY size DESC
LIMIT 50;

Anything from a deactivated plugin: kill it. Anything 100 KB or larger: investigate.

Measured impact: 20-80ms saved per request on a heavily-autoloaded site, more on shared hosting where every KB of memory matters.

Method 8: Strip WordPress core bloat

WordPress core itself ships things you may not need:

  • The emoji script (renders Unicode emoji as PNG on systems that can't render them natively — obsolete in 2026)
  • wp-embed.min.js (lets your site be oEmbedded by other sites)
  • jquery-migrate.js (compatibility shim for older jQuery code)
  • dashicons.css on the frontend (icon font for the admin, leaked to frontend by some themes)
  • XML-RPC API (/xmlrpc.php) — historic vector for brute-force attacks, useful only if you use the WordPress mobile app or remote publishing

Each one is a one-liner. Combined they typically save 30-80 KB and several requests per page:

remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('wp_print_styles', 'print_emoji_styles');
wp_deregister_script('wp-embed');

Every "frontend optimization" plugin ships these toggles. Doing them by hand is free.

Method 9: Object cache (yes, but bear with me)

Strictly speaking this is caching. But it's a different layer from page caching: it stores database query results in memory (Redis, Memcached), not rendered HTML on disk.

The advantage for our scenario: object cache speeds up logged-in users, REST API calls, admin requests — all the things page cache can't touch. WordPress's internal get_option, get_post, get_term calls hit the object cache layer when one's configured.

Setup: install Redis or Memcached on your server, install the Redis Object Cache plugin (or Memcached drop-in), point WordPress at the backend. Done.

Measured impact: on a typical WooCommerce site doing logged-in traffic, 30-50% reduction in TTFB. The single biggest "non-page-cache" win on this list.

The catch: requires server access. Shared hosts often don't expose Redis or Memcached. If you're on managed WordPress hosting (Kinsta, WP Engine, etc.), object cache is usually already enabled — check before assuming you need to set it up.

Putting it together

If you're starting from "no optimization at all" and want a structural baseline before any cache plugin enters the picture:

  1. Strip core bloat (Method 8) — 10 minutes, saves 30-80 KB
  2. Throttle the heartbeat (Method 4) — 2 minutes, cuts server load
  3. Dequeue obvious unused CSS/JS (Method 3) — 1-2 hours, saves 100-500 KB per page
  4. Optimize autoloads (Method 7) — 30 minutes, saves 20-80ms TTFB
  5. Set up object cache (Method 9) — 1 hour with server access, saves 30-50% TTFB on logged-in requests

Half a day of work. Compounding gains: a typical bloated WordPress site can shave 40-60% off uncached TTFB without installing a single new "speed plugin."

Methods 1, 2, and 6 — per-page plugin loading, hook pruning, query memoization — are the multiplier on top of that. They're also the hardest to implement by hand, because they require a rule engine, page fingerprinting, and a way to undo bad rules safely. Five plugin-skip rules hand-written work great. Fifty rules across a real WooCommerce site is a maintenance nightmare.

Where AcceleratorWP fits

The disclosure I promised: I make AcceleratorWP, which is a structural performance plugin that does Methods 1, 2, 3, and 6 as a unified system. Not because the techniques are secret — they're all in this post, and you can implement every single one by hand — but because doing them at scale is the kind of work where you want a wizard to suggest rules, a soak window to test them safely, and a way to revert when something breaks.

If you have 5-10 plugins and the patience to write some mu-plugin code, you don't need us. The methods above will get you most of the way there.

If you have 15+ plugins and you can't justify spending three weeks hand-writing rules and watching them break Elementor on pages you didn't think about, that's the gap we fill. The wizard fingerprints your site, suggests rules per page, lets you accept or reject them, and rolls them back instantly when something goes wrong. Measured improvement on a typical WC + Elementor + Bookly + MailPoet site, with wizard defaults only, on PageSpeed Insights mobile: −31% LCP, −54% Speed Index, TBT dropped from 90ms to 0. No caching layer involved.

Either way, the takeaway stands: the answer to "why is my WordPress site slow" almost never is "you need a cache plugin." It's "WordPress is doing too much work on every request." The fix is to do less work, not to memoize more responses.

Cache plugins are still useful — they sit on top of these techniques and store the result. But they're the second layer, not the first. The first layer is making the underlying request cheap. Once that's done, caching is the icing.

If you tried any of these and they helped — or broke things — I'd genuinely like to hear about it. Send notes to my inbox.

Free PDF

The 50-step WordPress performance cheatsheet.

The list I wish I'd had when I started tuning client sites — every step works without our plugin. Drop your email and the PDF lands in your inbox in under a minute. You'll also get the biweekly field notes; unsubscribe on the first click of the first email if it's not for you.

SE

Sarp Efe

Founder & developer · AcceleratorWP

Get Accelerator