DEV Community

Cover image for A practical walkthrough of real-world WordPress performance optimizations
Abiodun Paul Ogunnaike
Abiodun Paul Ogunnaike

Posted on

A practical walkthrough of real-world WordPress performance optimizations

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:

  1. Gutenberg Editor Dependencies Leaking to the Frontend: Public-facing custom blocks were pulling in entire WordPress admin and React libraries.
  2. Cache Busting via time(): Styles and scripts had dynamic timestamps, entirely defeating browser and CDN caching.
  3. Render-Blocking Web Fonts via CSS @import: Google Fonts were being pulled synchronously inside multiple stylesheets.
  4. Synchronous jQuery & jquery-migrate: Core jQuery was blocking the critical rendering path.
  5. 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
);
Enter fullscreen mode Exit fullscreen mode

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/me and rest-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,
    ]
);
Enter fullscreen mode Exit fullscreen mode

Key Takeaway: Reserve 'wp-blocks', 'wp-element', and 'wp-editor' exclusively for enqueue_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());
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

When a browser encounters @import:

  1. It pauses CSS parsing.
  2. It makes a blocking network roundtrip to fetch Google's CSS stylesheet.
  3. Only then does it discover the actual font files (.woff2) and begin downloading them.
  4. If multiple stylesheets include @import, this blocking waterfall repeats.

The Solution

  1. Remove all @import url(...) rules from your stylesheets and SCSS files.
  2. Add preconnect hints in your theme's functions.php to 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);
Enter fullscreen mode Exit fullscreen mode
  1. Enqueue the font centrally in <head> with display=swap:
wp_enqueue_style(
    'your-website-google-fonts',
    'https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap',
    array(),
    null
);
Enter fullscreen mode Exit fullscreen mode

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']
        );
    }
});
Enter fullscreen mode Exit fullscreen mode

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,
        ]
    );
});
Enter fullscreen mode Exit fullscreen mode

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
    }
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 @import fonts, dequeuing jquery-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)