Less WordPress plugins will nearly always return a better performing website. But the reality is the plugin causing all the trouble is usually the plugin that powers critical features on your website.
Removing it is not an option.
How often a plugin queries the database, and whether those queries are cached has a greater impact on the speed of your site than the number of plugins enabled.
Some common symptoms of plugins making a high number of queries;
How often a plugin queries the database and whether those queries are cached, has a greater impact on the speed of your site than the number of plugins enabled.
As if to prove my point, the first thing I do when looking to optimise a site is to install another plugin 😂. Usually that plugin is Query Monitor. It should be noted that once this optimisation is complete, Query Monitor should be disabled/removed.
Query Monitor allows you to see the number of queries happening on each page and where those queries are coming from. This gives you an insight into the problem areas and the root causes.
What you’re looking for here are opportunities: duplicate, slow, or uncached queries.
If you notice a high number or duplicate queries and the plugin could be replaced with little effort, consider reviewing alternatives to the plugin making the high or duplicate queries. It might be that there is a more compact plugin that exists which is more optimised for your use case.
If there isn’t an alternative, or the effort to replace the plugin is too much, consider the following:
Here are some examples of excessive queries that you may find in a plugin or theme that utilises a plugin’s functionality:
❌ Bad
$title = get_post_meta($post->ID, 'title', true);
$image = get_post_meta($post->ID, 'image', true);
$link = get_post_meta($post->ID, 'link', true);
The above counts as three individual database queries. Instead you could do something like:
✅ Good
$meta = get_post_meta($post->ID); // single database query
$title = $meta['title'][0] ?? '';
$image = $meta['image'][0] ?? '';
$link = $meta['link'][0] ?? '';
This counts as a single query.
Each looped item is a database query.
❌ Bad
foreach ($posts as $post) {
$author = get_user_meta($post->post_author);
}
An alternative might be to cache all relevant data with a single request before looping e.g.
✅ Good
$authorIDs = array_unique( wp_list_pluck($posts, 'post_author') ); // grab the relevant IDs only
cache_users($authorIDs); // single request, caching data
foreach ($posts as $post) {
$author = get_user_meta($post->post_author); // this request now hits cache, and does not make x database queries
}
How wp_query is used to make a database query can also impact the quality of the call.
❌ Bad
$query = new WP_Query([
'posts_per_page' => -1,
]);
Better option is to limit the results e.g.
✅ Good
$query = new WP_Query([ 'posts_per_page' => 10, ]);
If you’re using any of the following:
You can use caching techniques to store data and/or html in a single place, avoiding multiple/any calls to the database.
For example, for Archive pages you might implement one or multiple of the following:
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
$cacheKey = 'archive_featured_articles';
$html = get_transient($cacheKey);
if (!$html) {
$posts = get_posts([
'post_type' => 'post',
'posts_per_page' => 3,
]);
ob_start();
foreach ($posts as $post) {
get_template_part(
'template-parts/featured-card',
null,
['post' => $post]
);
}
$html = ob_get_clean();
set_transient($cacheKey, $html, HOUR_IN_SECONDS);
}
echo $html;
Navigation menus can be especially taxing if:
To combat this, consider utilising transient caching.
1. Check if transient is set, and display it.
$siteId = get_current_blog_id();
$cacheKey = 'menu_' . $siteId . '_' . md5($location . serialize($args));
$menu = get_transient($cacheKey);
if ($menu !== false) {
return $menu;
}
2. If no transient exists, generate the html and store it in the transient.
$defaults = [
'theme_location' => $location,
'echo' => false,
'container' => false,
];
$menu = wp_nav_menu(array_merge($defaults, $args));
set_transient($cacheKey, $menu, DAY_IN_SECONDS);
return $menu;
}
3. For maintaining this, clear the cached menu when the menu is updated (hooking into wp_update_nav_menu).
add_action('wp_update_nav_menu', function () {
global $wpdb;
$transients = $wpdb->get_col("
SELECT option_name
FROM {$wpdb->options}
WHERE option_name LIKE '_transient_menu_%'
");
foreach ($transients as $transient) {
$key = str_replace('_transient_', '', $transient);
delete_transient($key);
}
});
Sometimes optimisations aren’t enough.
🚩 Here are some red flags to watch out for:
Fewer plugins does not always mean an improvement to WordPress performance. In most instances you just need to reduce the number of database queries. Small changes in caching and query structure can dramatically reduce load without rewriting functionality or replace plugins.
Tired of clients seeing outdated CSS and JavaScript? This guide shows how to implement smart cache busting that works automatically based on your WordPress environment.
Read more ->
A personal reflection on artificial intelligence, creativity, and work—exploring fear, ethics, and why the human journey behind art still matters in the age of A.I.
Read more ->
Learn why committing compiled CSS and JavaScript to Git creates merge conflicts, deployment risk, and unreliable builds — and how to manage build artifacts safely with CI/CD.
Read more ->