When running a Google PageSpeed Insights or Lighthouse audit on a custom WordPress website, seeing a low mobile / desktop score (such as 30β40) alongside warnings like "Eliminate render-blocking resources", "Reduce unused JavaScript", and "Serve static assets with an efficient cache policy" is frustratingly common.
Many developers assume the only remedy is installing another heavyweight caching plugin. However, true long-term performance gains come from fixing fundamental bottlenecks in the theme itself: eliminating script bloat, optimizing font delivery, utilizing modern script loading strategies, and conditionally loading assets.
In this article, we'll walk through the exact steps we implemented on your-website to trim megabytes of unnecessary scripts, eliminate render-blocking delays, and streamline WordPress asset delivery.
The Initial Audit: What Went Wrong?
Our initial Lighthouse speed test revealed several common anti-patterns:
- Gutenberg Editor Dependencies Leaking to the Frontend: Public-facing custom blocks were pulling in entire WordPress admin and React libraries.
-
Cache Busting via
time(): Styles and scripts had dynamic timestamps, entirely defeating browser and CDN caching. -
Render-Blocking Web Fonts via CSS
@import: Google Fonts were being pulled synchronously inside multiple stylesheets. -
Synchronous jQuery &
jquery-migrate: Core jQuery was blocking the critical rendering path. - Redundant & Global Plugin Overhead: Heavy plugin assets were loading globally across 100% of pages, even where completely unused.
Here is how we addressed each issue step by step.
Step 1: Strip Gutenberg Admin Dependencies from Frontend Block Scripts
WordPress custom blocks created with the Block API often share registration logic between the editor and the frontend. A common oversight is passing editor packages into the frontend dependency array:
The Problem
// Bad: Loading editor runtime on public pages
wp_enqueue_script(
'your-website-banner-block',
get_template_directory_uri() . '/assets/scripts/banner.js',
['wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'jquery'], // π Heavy bloat!
YOUR_THEME_VERSION,
true
);
When you specify 'wp-blocks', 'wp-element', or 'wp-editor' as dependencies:
- WordPress automatically enqueues React,
react-dom,lodash, and the complete Gutenberg block parser runtime. - It injects API fetch handlers and triggers blocking background requests (
/wp-json/wp/v2/users/meandrest-nonce). - The payload sent to visitors increases by over 1.5β2.0 MB of unneeded JavaScript.
The Solution
Audit your frontend scripts. If a script only handles DOM manipulation, sliders, or UI toggles on the frontend, it does not need WordPress editor packages.
// Good: Only declare what the frontend script actually uses
wp_enqueue_script(
'your-website-banner-block',
get_template_directory_uri() . '/assets/scripts/banner.js',
['jquery'], // or [] if refactored to vanilla JavaScript
YOUR_THEME_VERSION,
[
'strategy' => 'defer',
'in_footer' => true,
]
);
Key Takeaway: Reserve
'wp-blocks','wp-element', and'wp-editor'exclusively forenqueue_block_editor_assets(the admin editor screen). Keep frontend enqueues lean.
Step 2: Stop Using time() for Asset Versioning
During development, itβs tempting to pass time() as the version argument to avoid caching while making frequent changes. However, leaving it in production is detrimental to performance.
The Problem
// Bad: Generates a new query string on every single request
wp_enqueue_style('your-website-style', get_stylesheet_uri(), array(), time());
This generates URLs like style.css?ver=1726312489. Because the version changes every second:
- Browsers never cache your CSS or JS files.
- Edge caches (Cloudflare, Fastly, etc.) treat every visit as a cache miss.
- Repeat visitors must re-download the entire stylesheet and script bundle on every page view.
The Solution
Use a centralized, static theme version constant (or file modification time if strictly needed during staging):
// In functions.php
if (!defined('YOUR_THEME_VERSION')) {
define('YOUR_THEME_VERSION', '1.0.4');
}
// In your enqueue functions
wp_enqueue_style('your-website-style', get_stylesheet_uri(), array(), YOUR_THEME_VERSION);
Whenever you deploy an update, increment YOUR_THEME_VERSION once. Browsers will cache assets indefinitely and only re-fetch them when you release a new version.
Step 3: Eliminate CSS @import & Modernize Font Loading
Loading web fonts via CSS @import declarations is one of the biggest contributors to slow Largest Contentful Paint (LCP) and First Contentful Paint (FCP).
The Problem
// In SCSS or CSS
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap");
When a browser encounters @import:
- It pauses CSS parsing.
- It makes a blocking network roundtrip to fetch Google's CSS stylesheet.
- Only then does it discover the actual font files (
.woff2) and begin downloading them. - If multiple stylesheets include
@import, this blocking waterfall repeats.
The Solution
-
Remove all
@import url(...)rules from your stylesheets and SCSS files. -
Add preconnect hints in your theme's
functions.phpto resolve the DNS and TLS handshake early:
add_filter('wp_resource_hints', function ($hints, $relation_type) {
if ('preconnect' === $relation_type) {
$hints[] = [
'href' => 'https://fonts.googleapis.com',
'crossorigin' => 'anonymous',
];
$hints[] = [
'href' => 'https://fonts.gstatic.com',
'crossorigin' => 'anonymous',
];
}
return $hints;
}, 10, 2);
-
Enqueue the font centrally in
<head>withdisplay=swap:
wp_enqueue_style(
'your-website-google-fonts',
'https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap',
array(),
null
);
display=swap instructs the browser to immediately render text using a system fallback font, seamlessly swapping in the custom web font once loaded. This eliminates Flash of Invisible Text (FOIT).
Step 4: Defer jQuery and Strip jquery-migrate
Historically, WordPress loaded jQuery synchronously in the <head> to support legacy plugins that inject inline jQuery calls in the body. In modern setups, this severely delays page rendering.
1. Remove jquery-migrate on Frontend
jquery-migrate is only required for legacy plugins using deprecated jQuery 1.x APIs. Removing it saves unnecessary HTTP requests and ~10 KB of blocking script execution:
add_action('wp_default_scripts', function ($scripts) {
if (!is_admin() && !empty($scripts->registered['jquery'])) {
$scripts->registered['jquery']->deps = array_diff(
$scripts->registered['jquery']->deps,
['jquery-migrate']
);
}
});
2. Defer Core jQuery (WordPress 6.3+)
WordPress 6.3 introduced native script loading strategies (defer and async). You can instruct WordPress to defer jQuery on non-admin pages:
add_action('wp_enqueue_scripts', function () {
if (!is_admin()) {
wp_script_add_data('jquery', 'strategy', 'defer');
wp_script_add_data('jquery-core', 'strategy', 'defer');
}
// Defer your theme's custom scripts as well
wp_enqueue_script(
'your-website-global',
get_template_directory_uri() . '/assets/scripts/global.js',
['jquery'],
YOUR_THEME_VERSION,
[
'strategy' => 'defer',
'in_footer' => true,
]
);
});
3. Safeguard Inline Event Handlers
If your theme renders inline scripts (such as reCAPTCHA or tracking handlers in wp_footer), avoid calling jQuery(document).ready(...) directly in raw <script> tags, because deferred jQuery will not have executed yet when the HTML parser hits that inline tag.
Instead, use standard Vanilla JavaScript:
// Instead of jQuery(document).on('submit', 'form', handler):
document.addEventListener('submit', function (event) {
if (event.target && event.target.tagName === 'FORM') {
// Execute form logic safely
}
});
Step 5: Dequeue Redundant Third-Party Plugin Assets
As custom themes evolve, features originally handled by plugins (such as back-to-top buttons, social sharing buttons, or mobile drawers) are frequently rewritten as native, lightweight theme components.
However, inactive or redundant plugins often remain installed and continue injecting styles and scripts into every page.
add_action('wp_enqueue_scripts', function () {
// Dequeue redundant general plugin assets handled natively by your-website
wp_dequeue_style('redundant-plugin-style');
wp_dequeue_style('redundant-plugin-fonts');
wp_dequeue_script('redundant-plugin-script');
}, 100);
By auditing active plugins and dequeuing obsolete stylesheets and scripts, you remove duplicate network payloads without affecting user experience.
Step 6: Conditionally Load Heavy Plugin Assets (Forms & Anti-Spam)
Popular plugins like contact form managers and security/spam verifiers often enqueue their JavaScript and CSS across 100% of your website's pages, even on simple text articles and archive pages where no forms exist.
The Problem
A visitor reading a blog post or an about page is forced to download form validation scripts, styling bundles, and reCAPTCHA libraries that will never be used on that page.
The Solution: Smart Conditional Dequeuing
Instead of loading form assets globally, only keep them active on pages where forms are actually present.
add_action('wp_enqueue_scripts', function () {
global $post;
// Check if the current page actually has a form
// (Be sure to check for custom popup banners or modals that embed forms!)
$has_form = is_front_page() || is_home() || (
is_a($post, 'WP_Post') && (
has_shortcode($post->post_content, 'contact-form-7') ||
has_block('contact-form-7/form', $post) ||
has_block('your-website/notification-bar-block', $post) ||
stripos($post->post_content, 'contact-form-7') !== false
)
);
// If no form is present, dequeue the form assets
if (!$has_form) {
wp_dequeue_script('contact-form-7');
wp_dequeue_style('contact-form-7');
wp_dequeue_script('form-anti-spam');
wp_dequeue_style('form-anti-spam-css');
}
}, 99);
Important Pro Tip: If your homepage or header contains an interactive popup banner or modal with an embedded registration form, ensure your condition explicitly includes
is_front_page()or checks for your banner block. Otherwise, selective dequeuing might strip the AJAX handlers from your popup form!
The Results
Applying these optimizations produced immediate, measurable improvements:
- Payload Reduction: Over 1.5 MB of unnecessary JavaScript (React & Gutenberg editor dependencies) was eliminated from the public frontend.
-
Render-Blocking Clearance: Removing
@importfonts, dequeuingjquery-migrate, and deferring core jQuery reduced initial render blocking by hundreds of milliseconds. -
Effective Caching: Replacing
time()with static versioning enabled proper browser caching and CDN hit ratios for all repeat visitors. - Zero Lost Functionality: Interactive blocks, form popups, and native animations continue to work seamlessly.
Conclusion
Optimizing WordPress performance doesn't require stripping away interactive features or relying solely on complex caching plugins. By auditing asset dependencies, modernizing script loading strategies, and delivering assets only where they are genuinely needed, you can deliver an ultra-fast experience to your visitors.
Have you audited your custom Gutenberg blocks and plugin enqueues recently? Let us know in the comments below!
Top comments (0)