Most “make WordPress faster” advice ends the same way: install a premium cache plugin, pay yearly, hope for the best. That works, but it also adds another layer of PHP to every request and hides the real problem. The truth is that you can speed up WordPress without plugins and reach good Core Web Vitals using nothing but your hosting panel, a few lines in .htaccess, a child theme functions.php file and a database cleanup.
This guide lists 11 fixes in order of impact. Each one includes the expected gain in milliseconds, the exact place to verify it in PageSpeed Insights, and the code where code is needed. No paid tools, no affiliate links, no plugin installs.
Before you touch anything: measure properly
You cannot prioritise what you have not measured. Spend ten minutes here and you will save hours later.
- Run PageSpeed Insights on your homepage and on your two most visited inner pages (a blog post and a product or service page). Different templates load different assets.
- Write down four numbers for mobile: LCP, CLS, TBT and Server Response Time (found under Diagnostics as “Initial server response time”).
- Check the field data panel at the top if your site gets enough traffic. Lab scores fluctuate, real user data does not lie. Target values today are still LCP under 2.5s, INP under 200ms and CLS under 0.1.
- Test three times per URL and keep the median. A single run tells you almost nothing.
Useful free companions: WebPageTest (waterfall view, repeat-view test), Chrome DevTools > Network with throttling set to Fast 4G, and GTmetrix for a second opinion. All free, all browser based.

Priority table: what to fix first
| Fix | Typical gain | Effort | PageSpeed audit that should change |
|---|---|---|---|
| 1. Server-level page cache (host panel) | 200ms to 900ms TTFB | 5 min | Initial server response time |
| 2. PHP 8.3+ and OPcache | 80ms to 400ms | 5 min | Initial server response time |
| 3. Brotli or Gzip compression | 50ms to 300ms | 10 min | Enable text compression |
| 4. Browser cache headers | 300ms to 1.2s on repeat visits | 10 min | Serve static assets with an efficient cache policy |
| 5. Image discipline (size, format, lazy load) | 300ms to 1.5s LCP | 30 min | Properly size images, Serve images in next-gen formats |
| 6. Delete unused plugins and themes | 100ms to 1s | 20 min | Reduce unused JavaScript, Server response time |
| 7. Conditionally dequeue plugin CSS/JS | 100ms to 500ms | 30 min | Reduce unused CSS/JavaScript |
| 8. Defer non-critical JavaScript | 150ms to 600ms TBT | 15 min | Eliminate render-blocking resources |
| 9. Self-host and preload fonts | 100ms to 400ms LCP | 30 min | Ensure text remains visible during webfont load |
| 10. Database cleanup | 20ms to 150ms per request | 15 min | Initial server response time |
| 11. Strip core bloat and throttle Heartbeat | 30ms to 120ms | 10 min | Reduce unused JavaScript, fewer requests |
Back up your site and database before editing any file. Put every snippet below in a child theme functions.php or in a small custom file, never in a parent theme that will be updated.
1. Turn on caching at the hosting level, not in PHP
This is the single biggest win and the reason a cache plugin is often unnecessary. Almost every serious host now ships a server-side full page cache that answers requests before WordPress even boots.
- Nginx FastCGI cache or Varnish on VPS and managed hosts
- LiteSpeed / OpenLiteSpeed hosts expose an object and page cache in cPanel
- Most managed WordPress platforms label it “Full page cache”, “Edge cache” or “Static cache” in the dashboard
- Add Redis or Memcached object caching if your host offers it as a one-click toggle, this cuts repeated database queries on logged-in and WooCommerce pages
Expected gain
Server response time typically drops from 600ms to 900ms down to 80ms to 200ms. On a shared host with no cache at all, gains above 1 second are common.
How to verify
In PageSpeed Insights, watch Initial server response time under Diagnostics. In DevTools, open the Network tab, click the document request and look for a response header such as x-cache: HIT, x-litespeed-cache: hit or cf-cache-status: HIT.
Do not forget exclusions
Exclude cart, checkout, account and any logged-in session from full page caching, otherwise you will serve one user’s cart to another.
2. Move to PHP 8.3 or newer and confirm OPcache is on
Older PHP versions are slower and, in the case of PHP 8.1 and below, no longer receive security patches. Modern PHP executes the same WordPress code noticeably faster.
- Open your hosting panel and find PHP version or PHP selector.
- Switch to PHP 8.3 or 8.4. Test the front end and admin immediately, and roll back if a legacy plugin breaks.
- In the same panel check that OPcache is enabled, and raise
memory_limitto 256M andmax_execution_timeto 120 if you can.
Expected gain: 80ms to 400ms of server time, more on query-heavy pages. Verify: Initial server response time in PSI, plus Tools > Site Health in WordPress which will stop nagging you about an outdated PHP version.
3. Enable Brotli or Gzip text compression
HTML, CSS, JavaScript and SVG compress beautifully, often by 70 to 80 percent. Many hosts already enable it, many shared hosts do not.
On Apache, add this to .htaccess:
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
AddOutputFilterByType DEFLATE application/javascript application/json
AddOutputFilterByType DEFLATE image/svg+xml application/xml
</IfModule>
On Nginx you need gzip on; plus gzip_types in the server block, or ask support to enable Brotli, which compresses roughly 15 to 20 percent better than Gzip. If your site sits behind a free Cloudflare plan, Brotli is applied at the edge automatically.
Expected gain: 50ms to 300ms, higher on page-builder sites with huge CSS files. Verify: the Enable text compression audit disappears from PSI, and the response header content-encoding: br or gzip appears in DevTools.

4. Set proper browser cache headers
Repeat visitors should download nothing but HTML. This is pure configuration, no plugin required. minddigital.com makes the same point with more data.
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/avif "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType text/html "access plus 0 seconds"
</IfModule>
<IfModule mod_headers.c>
<FilesMatch "\.(css|js|woff2|jpg|jpeg|png|webp|avif|svg)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
</IfModule>
A one year expiry is safe because WordPress appends a ?ver= query string to theme and plugin assets, so updates bust the cache automatically.
Expected gain: 300ms to 1.2s on second page views. Verify: run a repeat view test in WebPageTest and confirm the “Serve static assets with an efficient cache policy” audit is gone.
5. Fix your images, this is usually your LCP
On most WordPress sites the Largest Contentful Paint element is a hero image or featured image. Image work is where the seconds are.
What to do
- Stop uploading oversized files. Resize to the maximum width your layout actually uses (1600px to 1920px for full-width hero, 800px for in-content) before uploading. WordPress will generate the smaller sizes and a
srcsetfor you. - Convert to WebP or AVIF with free tools: Squoosh in the browser, or
cwebp -q 75 image.jpg -o image.webpover SSH. Recent WordPress versions handle WebP uploads natively. - Keep native lazy loading for below-the-fold images, WordPress adds
loading="lazy"by itself. - Never lazy load the hero. Core adds
fetchpriority="high"to the likely LCP image, but page builders sometimes break it. If your hero is lazy loaded, disable it for that template:
add_filter( 'wp_lazy_loading_enabled', function( $default, $tag_name, $context ) {
if ( 'img' === $tag_name && 'the_content' === $context && is_front_page() ) {
return false;
}
return $default;
}, 10, 3 );
- Always output width and height attributes so the browser reserves space. This alone can take CLS from 0.25 to under 0.05.
Expected gain: 300ms to 1.5s of LCP, plus a large CLS improvement. Verify: “Properly size images”, “Serve images in next-gen formats”, “Image elements do not have explicit width and height” and the LCP element breakdown in the PSI diagnostics panel.
6. Delete unused plugins and themes, then audit what remains
Deactivated plugins still sit in your filesystem and remain an attack surface, and active ones you forgot about load CSS and JS on every page. For the wider picture, see Optimizing Your WordPress Site Without Plugins.
- Go to Plugins > Installed Plugins and delete anything deactivated.
- Delete all themes except your active theme and one default fallback.
- For every remaining plugin ask: does this run on the front end, and would a 10 line snippet replace it? Social share buttons, custom code injectors, “insert header script” helpers and simple contact forms are frequent candidates for removal.
- Identify heavy plugins by deactivating one at a time in a staging copy and rerunning PSI. Slow admin ajax, session cookies and per-request remote API calls are the usual culprits.
Expected gain: 100ms to 1s depending on what you remove. Sliders, related-post engines and analytics wrappers are the worst offenders. Verify: request count and total byte weight in PSI, plus “Reduce unused JavaScript”.
7. Dequeue plugin CSS and JS on pages that do not need them
A contact form plugin loading its assets on 400 blog posts is wasted bandwidth. WordPress lets you unload them with a few conditions, no optimisation plugin involved.
add_action( 'wp_enqueue_scripts', function() {
// Contact form assets only on the contact page
if ( ! is_page( 'contact' ) ) {
wp_dequeue_style( 'contact-form-7' );
wp_dequeue_script( 'contact-form-7' );
}
// Core block CSS only where blocks are used
if ( is_page( 'landing-classic' ) ) {
wp_dequeue_style( 'wp-block-library' );
wp_dequeue_style( 'wp-block-library-theme' );
wp_dequeue_style( 'classic-theme-styles' );
wp_dequeue_style( 'global-styles' );
}
}, 100 );
Find the exact handles by viewing the page source and reading the id attribute of each <link> and <script> tag, for example id="contact-form-7-css" means the handle is contact-form-7.
Expected gain: 100ms to 500ms and 50KB to 300KB less per page. Verify: DevTools > Coverage tab shows unused bytes dropping, and “Reduce unused CSS” shrinks in PSI. Click through every affected template afterwards, dequeuing too aggressively breaks layouts.

8. Defer non-critical JavaScript
Render-blocking scripts in the head delay first paint. Adding defer to everything except the handful of scripts that must run early is a two minute change.
add_filter( 'script_loader_tag', function( $tag, $handle ) {
if ( is_admin() || is_customize_preview() ) {
return $tag;
}
$no_defer = array( 'jquery-core', 'jquery-migrate' );
if ( in_array( $handle, $no_defer, true ) ) {
return $tag;
}
if ( false !== strpos( $tag, ' defer' ) || false !== strpos( $tag, ' async' ) ) {
return $tag;
}
return str_replace( ' src=', ' defer src=', $tag );
}, 10, 2 );
Add any handle that breaks to the $no_defer array. Test menus, sliders, tabs, forms and cart behaviour after deploying.
Expected gain: 150ms to 600ms of Total Blocking Time, and often a visible FCP improvement. Verify: “Eliminate render-blocking resources” and “Reduce JavaScript execution time” in PSI.
Bonus: free speculative loading, already in core
Recent WordPress versions include the Speculation Rules API, which prefetches or prerenders the next page on hover. Near-instant navigation, zero plugins:
add_filter( 'wp_speculation_rules_configuration', function( $config ) {
return array(
'mode' => 'prerender',
'eagerness' => 'moderate',
);
} );
9. Self-host your fonts, preload them, and use font-display: swap
Third party font calls add a DNS lookup, a TLS handshake and a render delay. Hosting the files yourself removes all three and is also friendlier for privacy compliance.
- Download the woff2 files you actually use. Two weights maximum, for example 400 and 700. Each extra weight is another 20KB to 40KB of critical path.
- Upload them to
/wp-content/themes/your-child-theme/fonts/. - Declare them in CSS with
font-display: swap;so text renders immediately in a fallback font. - Preload only the font used in the first viewport:
add_action( 'wp_head', function() {
$url = get_stylesheet_directory_uri() . '/fonts/inter-400.woff2';
echo '<link rel="preload" href="' . esc_url( $url ) . '" as="font" type="font/woff2" crossorigin>';
}, 1 );
Also remove the theme’s Google Fonts enqueue, otherwise you load both sets:
add_action( 'wp_enqueue_scripts', function() {
wp_dequeue_style( 'theme-google-fonts' );
}, 100 );
Expected gain: 100ms to 400ms of LCP and no flash of invisible text. Verify: “Ensure text remains visible during webfont load” and “Reduce the impact of third-party code” in PSI.
10. Clean and optimise the database
Years of revisions, expired transients, spam comments and orphaned metadata bloat wp_options and wp_postmeta, the two tables WordPress hits on every single request. A bloated autoload option set is a classic hidden cause of slow TTFB.
With WP-CLI (cleanest method)
wp post delete $(wp post list --post_type=revision --format=ids) --force
wp transient delete --all
wp transient delete --expired
wp comment delete $(wp comment list --status=spam --format=ids) --force
wp db optimize
With phpMyAdmin (back up first)
-- inspect the biggest autoloaded options
SELECT option_name, LENGTH(option_value) AS size
FROM wp_options
WHERE autoload = 'yes'
ORDER BY size DESC
LIMIT 20;
-- remove orphaned post meta
DELETE pm FROM wp_postmeta pm
LEFT JOIN wp_posts p ON p.ID = pm.post_id
WHERE p.ID IS NULL;
Then cap future revisions in wp-config.php:
define( 'WP_POST_REVISIONS', 5 );
define( 'EMPTY_TRASH_DAYS', 7 );
Expected gain: 20ms to 150ms per uncached request, far more on sites with a 200MB options table. Verify: Initial server response time, and compare admin dashboard responsiveness before and after.
11. Strip WordPress core bloat and throttle Heartbeat
Core ships features you may never use. Each one adds a request or a script.
// Emoji script and styles
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
remove_action( 'admin_print_styles', 'print_emoji_styles' );
add_filter( 'emoji_svg_url', '__return_false' );
// oEmbed discovery and wp-embed.js
remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
remove_action( 'wp_head', 'wp_oembed_add_host_js' );
add_action( 'wp_footer', function() {
wp_deregister_script( 'wp-embed' );
} );
// Head clutter
remove_action( 'wp_head', 'rsd_link' );
remove_action( 'wp_head', 'wp_generator' );
remove_action( 'wp_head', 'wp_shortlink_wp_head' );
// Slow down the admin Heartbeat API
add_filter( 'heartbeat_settings', function( $settings ) {
$settings['interval'] = 60;
return $settings;
} );
Also disable pingbacks under Settings > Discussion and block xmlrpc.php at server level if nothing you use depends on it. Brute force attempts against XML-RPC are a real source of server load on shared hosting.
Expected gain: 30ms to 120ms and three to five fewer requests. Verify: request count in the PSI network payload table, and admin-ajax frequency in DevTools.

Free extras worth ten more minutes
- Cloudflare free plan: global CDN, Brotli, HTTP/3 and TLS at no cost. Point your nameservers, keep the proxy on for static assets, and set a cache rule. Expect 100ms to 500ms for visitors far from your server.
- Preconnect to unavoidable third parties:
<link rel="preconnect" href="https://www.googletagmanager.com">saves a DNS and TLS round trip. - Audit your tracking scripts: one analytics tool, not four. Tag Manager containers full of dormant tags are often the single biggest source of blocking time.
- Trim the page itself: 12 posts in a blog archive instead of 30, no autoplay video hero, no carousel with eight full-size slides.
What is not worth your time
- Chasing a 100 score. Google ranks on field Core Web Vitals, not on lab scores. A green LCP at score 84 beats a fragile 98 that broke your navigation.
- Aggressive CSS/JS concatenation on HTTP/2 and HTTP/3. Multiplexing means many small files are fine, and merging everything often makes caching worse.
- Stacking two cache layers. Server cache plus a plugin cache plus edge cache produces stale pages and debugging nightmares.
- Removing jQuery from a theme that depends on it. The 30ms you save is not worth a broken menu.
A realistic before and after
| Metric (mobile) | Before | After the 11 fixes |
|---|---|---|
| Server response time | 780ms | 140ms |
| LCP | 4.6s | 2.1s |
| Total Blocking Time | 620ms | 110ms |
| CLS | 0.22 | 0.02 |
| Page weight | 3.4MB | 0.9MB |
| Performance score | 38 | 89 |
These are typical numbers for a business site on a decent shared host with a page builder theme. The exact figures on your site will differ, which is precisely why you measure after every single change instead of doing all 11 at once.
Recommended order of work
- Back up, then create a staging copy if your host offers one for free.
- Fixes 1 and 2 (hosting cache, PHP). Retest.
- Fixes 3 and 4 (compression, cache headers). Retest.
- Fix 5 (images). Retest, this is often where LCP turns green.
- Fixes 6 and 7 (plugin cleanup, dequeue). Click every template.
- Fixes 8 and 9 (defer, fonts). Test forms, menus and sliders carefully.
- Fixes 10 and 11 (database, core bloat).
- Add Cloudflare, then wait 28 days and check field data in PageSpeed Insights.
FAQ
Can you really speed up a WordPress site without any plugin?
Yes, for the vast majority of sites. Page caching, compression, cache headers and a CDN all live at the server or edge level, and the rest of the work (images, fonts, script deferral, database) is either a content habit or a few lines of code. Plugins mainly add convenience and a user interface, not capability.
How do I improve my PageSpeed score without a cache plugin?
Start with the two audits that hold the most weight: Initial server response time (fix with host-level caching and modern PHP) and the LCP element (fix with properly sized WebP images and a preloaded font). Then reduce render-blocking resources with defer and dequeue assets you do not use. That covers most of the points available.
Is a free WordPress speed plugin better than doing this manually?
A free cache plugin is a reasonable option if you have no server access at all, for instance on very restrictive shared hosting. But it runs inside PHP, so it can never be as fast as a cache that answers before WordPress loads. If your host offers server-side caching, use that instead and keep your plugin count low.
Is WordPress outdated in 2026?
No. It still powers roughly 40 percent of the web, and recent releases added genuinely useful performance features in core, including native lazy loading, fetchpriority on the LCP image, WebP support and speculative loading for instant navigation. What is outdated is the habit of solving every problem with another plugin.
How many plugins is too many?
There is no magic number. Ten lightweight, well-coded plugins can be faster than three bloated ones. What matters is the front-end footprint: how many CSS and JS files each one enqueues, whether it makes external HTTP calls on page load, and how many rows it adds to autoloaded options.
How long until Google notices my site is faster?
PageSpeed Insights lab data updates instantly. The field data section reflects a rolling 28 day window of real Chrome users, so allow roughly a month before Core Web Vitals in Search Console turn green.
My score is still low after all of this. What now?
At that point the bottleneck is usually the theme or page builder itself, or a single heavy third party script. Test a default block theme on staging: if the score jumps 30 points, your theme is the problem and no amount of tweaking will fix it. Rebuilding on a lean theme beats fighting a heavy one.
Bottom line: you do not need a premium plugin licence to hit good Core Web Vitals. You need a host with server-side caching, modern PHP, disciplined images, a short plugin list and about two hours of focused work. Measure before, measure after, and fix in order of impact.

