Fast WordPress: 20 directives instead of theory

8 min readWordPressperformanceCore Web Vitals

What to do, what not to do, and in what order. With code you can copy, and with the reason analytics almost never hurts LCP - and plugins are not the culprit.

Under one of our posts a developer asked us to write up what we had learned, with a condition we think is the smartest one possible: he wanted a set of directives, not another few hours of theory. Here it is - twenty items, each of which we do by hand on real projects.

WordPress is not the glutton. Bare core serves a page fast. What makes a site slow is a builder-assembled theme and third-party scripts nobody looks at.

First, agree on what you are measuring

1. Lighthouse is a laboratory; for ranking, Google looks at field data. The mobile test emulates a slow CPU and moves by several points between runs with no code change at all. What counts is the real LCP, INP and CLS in your visitors' browsers. The score in the report affects positions not at all.

2. Measure every template type, not the homepage. The homepage is always polished. The trouble lives on the single post, in paginated listings, in search and in the cart. Run one URL of each type, or you will be optimising the shop window while people walk through the warehouse.

3. Separate the metrics by nature. LCP is the critical rendering path: what stops the largest element from being painted. INP is the main thread: how much JavaScript runs between the click and the response. CLS is reserved space. Three different illnesses, three different treatments. The commonest mistake is to see red and start «optimising everything».

Theme and plugins

4. A page builder is a tax you pay on every request. Elementor, WPBakery and the rest drag along their own CSS, their own JS and their own rendering layer on top of WordPress. Removing the builder is the single largest win available. On its own it will not give you a hundred, though - it is half the road.

5. Plugins are not the enemy, and counting them proves nothing. A good plugin beats a bad hand-rolled version in the theme, and most of what is useful on a site arrives as one. If something cannot be done properly in the theme, take the plugin and do not feel bad about it.

6. The question is not «how many plugins» but what a given plugin prints to the front end where it is not needed. A form plugin loading its CSS and JS on every page while the form sits on one is not a bad plugin, it is a badly configured one. One hook fixes it.

add_action( 'wp_enqueue_scripts', function () {
    if ( is_page( 'contact' ) ) {
        return;
    }
    wp_dequeue_style( 'some-forms-plugin' );
    wp_dequeue_script( 'some-forms-plugin' );
}, 100 );
Priority 100, so this runs after the plugin has queued its own assets.

7. Remove what core prints by itself and you do not need: the emoji script, the generator tag, the RSD and WLW links, XML-RPC. Ten lines in the theme and a few fewer requests on every page.

remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'wp_head', 'wp_generator' );
remove_action( 'wp_head', 'rsd_link' );
remove_action( 'wp_head', 'wlwmanifest_link' );
add_filter( 'xmlrpc_enabled', '__return_false' );

Cache: pick the plugin by the stack

8. A page cache is mandatory, and which one is decided by the server rather than by preference.

  • LiteSpeed or OpenLiteSpeed - LiteSpeed Cache. The cache lives in the server itself: a request for a cached page never reaches PHP. The cheapest win there is, and free.
  • nginx or Apache - WP Rocket. Here the cache is inevitably PHP-level: the plugin writes static HTML and serves it before core boots.
  • The trap: if your host sells «LiteSpeed» but is really Apache with lsapi, LSCache falls back to PHP level and the main advantage evaporates. Check the response headers, not the pricing page.

9. Invalidate by event, not by timer. A week-long cache cleared on publish, on a comment or on a data change beats an hourly cache that expires on its own. The rule: every action that changes content must explicitly clear what it changed.

10. The «magic» options on top of the cache - combining and minifying CSS and JS, removing unused styles, deferring all JavaScript - work precisely when you do not control the code. On an inherited site with a builder and two dozen plugins that nobody is going to sit down and rewrite by hand, they are the best tool you have and they deliver real results. On a site with your own theme and a sane build they give almost nothing: the code is already minified, there is nothing to combine, and there is plenty to break. Switch them on one at a time and check the site after each - that is how you find the one that breaks it, instead of rolling all of them back together. Only one thing here comes without an «it depends»: never enable HTML minification in the plugin alongside your own compression at theme or server level - the response arrives corrupted and you will spend a week looking for the reason.

CSS, JavaScript, fonts

11. Load every script of your own with defer. It is a first-class argument when registering the script; the old script_loader_tag crutch is no longer needed. You should have no parser-blocking script at all.

wp_enqueue_script(
    'theme-main',
    get_theme_file_uri( '/assets/main.js' ),
    array(),
    filemtime( get_theme_file_path( '/assets/main.js' ) ),
    array( 'strategy' => 'defer', 'in_footer' => true )
);
filemtime as the version - the browser cache busts itself the moment the file changes.

12. Turn on per-block styles and split code by page type. By default core serves one large file covering every block; one filter and it serves only the styles of the blocks actually on the page. The same goes for your own code: what is needed on one page should load on one page. This is where half the excess weight in a typical project hides.

add_filter( 'should_load_separate_core_block_assets', '__return_true' );

13. Fonts locally only, woff2 only, font-display: swap, two families maximum. An external font service is a third-party domain in the critical path, costing you DNS, TCP and TLS before the first paint. Preload only the weight your heading is set in: preloading everything is not optimisation, it is competition for the same pipe.

Images. This is where the biggest win is

14. Prepare every size ahead of time as WebP rather than converting at runtime. On-the-fly conversion by a plugin is load on the host and latency for the first visitor.

15. One hero eager, everything else lazy. In core this is governed by the threshold after which lazy loading is applied. The default is 3. For layouts that open with a large banner, lower it to one.

add_filter( 'wp_omit_loading_attr_threshold', function () {
    return 1;
} );

16. Check which image core thinks is the LCP one, and preload it. Core sets high priority on the first large image by itself - and when a layout opens with a decorative strip or a logo, it gets that wrong. Read the rendered HTML rather than your intentions, and back it up with a preload using the same set of sizes you serve in the markup. The cheapest way to take half a second off your slowest metric.

add_filter( 'wp_preload_resources', function ( $resources ) {
    if ( ! is_front_page() ) {
        return $resources;
    }
    $resources[] = array(
        'href'          => get_theme_file_uri( '/assets/hero-1280.webp' ),
        'as'            => 'image',
        'fetchpriority' => 'high',
    );
    return $resources;
} );

17. Width and height, always. Without them the browser does not know how much space to reserve and the page jumps. That is the straight road to bad CLS - the kind a reader feels, not only a test.

Third-party scripts

18. Analytics almost never hurts LCP. This is the central misunderstanding of the subject. GA4 loads asynchronously and costs you INP and main-thread time, not the largest element. What hurts LCP is whatever sits in the critical path, and by frequency that is: fonts from someone else's domain, the consent banner, ads above the fold, the tag manager, and then support chats, maps and embedded video. Those last ones: facades only - an image with a play button, the player after the click.

19. Do not defer analytics to the first interaction if you want honest data. Loading it after a scroll or a click really does add points, but you lose everyone who left immediately - precisely the people you opened the statistics for. It is a trade, not an optimisation, and the owner makes this call, not the developer.

How to end the argument with the client

20. Measure twice: with third-party scripts and without. Block their domains in DevTools and run the test again. The difference between the two runs is the price your client is paying for analytics and ads.

Until the client has seen that difference as a number, a slow site is your fault. Once they have seen it, it is their decision and their trade-off.

This is the most important item on the list, and it is not a technical one. We know of no other way to close that conversation for good. And a week later, look at the field data in Search Console - the only assessment that means anything to search.

The order of operations

  • Throw out the builder and what core prints by itself.
  • Configure plugins so they do not print themselves where they are not needed.
  • Serve images and fonts properly.
  • Install the cache that matches your server, and use its «magic» options where the code is not yours.
  • Only at the end, negotiate over third-party scripts.

Start from the end and you will install a caching plugin on top of a page builder, then wonder why nothing changed.

And remember the main thing: a hundred on mobile with ads running is either a test without the ads or a lucky coincidence. Set yourself an honest budget and optimise what is genuinely within your power.