Autoloading WordPress options efficiently and responsibly

Autoloading options in WordPress

If you’re a WordPress plugin developer, you may have come across the concept of autoloading options. Or you may not, since knowing about autoloading options is, technically speaking, entirely optional when implementing a WordPress plugin that uses options. However, being unaware of the concept of autoloading options can lead to massive performance problems for large WordPress sites. It can notably slow down the server response of sites using your plugin. And by doing that, it hurts their “Time to First Byte” (TTFB), which directly contributes to the Core Web Vitals metric “Largest Contentful Paint” (LCP).

This post is about explaining what autoloading options in WordPress is, how it works, and how plugins should go about it. We’ll go over the basics as well as explore how to use more recently introduced WordPress Core enhancements, including some code examples. This will hopefully help you get to a comprehensive understanding of how to apply autoloading in your WordPress plugins in a way that’s both efficient (for your own plugin’s usage) and responsible (for the overall sites using your plugin, in a gazillion of combinations with other plugins).

What is autoloading options and why is it needed?

To understand what autoloading options is, you should first have a basic understanding of what WordPress options are. I’m not going to rehash this here, as there’s lots of resources on the web about that, for example the official WordPress Options API documentation. In a nutshell: WordPress options are arbitrary pieces of site-wide data that are persistently stored in the WordPress database, in the options table.

There is a ton of data that is stored in the options table. Even WordPress Core out of the box stores more than 100 options in that table. And many of these options are used in every single request to a WordPress site.

Options and their values are typically accessed via the get_option() function. On a regular WordPress site, without a persistent object cache in place, this in principle means that for every option that is read during a request a database request is made. Now relatively speaking, database requests are some of the slowest parts of generating the WordPress response to a user/browser request. Of course we cannot avoid them, as the database is the only common way to store data persistently – i.e. in a way so that the data is still available in subsequent requests. But it’s important for performance to use as few database requests as possible.

The number of database requests needed to serve a typical WordPress response varies greatly, but it probably shouldn’t be much higher than 10 or 20 requests. Now, going back to how many options there are and given how many of them are required in every request, imagine every option would require a single database request. That would increase the amount of database requests per response at least threefold, likely far more. In other words, it would be extremely inefficient and slow, so it’s something we need to avoid. And that’s where the concept of autoloading options comes in.

Autoloading options improves performance

Autoloading options effectively means that certain options are loaded using a single combined database request. This is done entirely for performance, for the reason outlined above. The way that it works is that there’s an allowlist of options that get autoloaded with a single bulk database request for every WordPress response. Then, once they’re accessed via get_option(), no additional database requests are made, instead they’re simply read from memory as they were already read from the database in that bulk database requests. This way it avoids the terrible bottleneck that reading every single option individually would mean.

The way this works under the hood is that every WordPress option in the database is marked with an indicator for whether or not it should be autoloaded, in the autoload column of the options table. The bulk database request that WordPress makes to autoload options then simply requests all options that have an autoload value which means either “autoload me!” or “don’t autoload me!”.

Autoloading too many options hurts performance

However, there’s a catch: There is no guarantee that every autoloaded option is actually used. So WordPress sites will usually end up with some options that get autoloaded unnecessarily. If one option is autoloaded unnecessarily, that’s not a real problem at all. Same if 10 options are autoloaded unnecessarily, possibly even if 100 options are autoloaded unnecessarily. The latter may already be a problem on less powerful WordPress hosting environments. But no matter the hosting environment, the reality for many WordPress sites is that there are 1000s of autoloaded options that come from all kinds of different plugins, themes, sometimes even plugins that were used once and then deleted. And that’s where server-side performance really suffers: Of course, the goal of autoloading options is to improve performance. But if that leads to autoloading tons of options that are not actually needed to generate the WordPress response, then that overhead can make the one bulk database request much slower than individual requests to the options that are needed would have been. And at that point, we’ve accomplished the exact opposite of what we wanted to achieve – we’ve slowed down WordPress’s server-side performance.

It is worth highlighting that excessive autoloading can even lead to bugs that cause much bigger performance problems: For WordPress sites using Memcache as a persistent object cache layer, there may be size limits on how large a single cache value can be. If the autoloaded option values are cumulatively too large, e.g. larger than 1MB, this may result in the value never being successfully stored in the cache. And that in turn will lead to the autoloaded options database request to always be sent, which for high-traffic sites that typically use a persistent object cache can be fatal. You may now question when the autoloaded options list would ever grow beyond 1MB. But it does happen, as sites use many plugins and accumulate lots of historical data over time.

We can however do our best as an ecosystem to avoid any of these issues by responsibly autoloading options.

Which options should be autoloaded?

In order to avoid excessive autoloading, it’s crucial that plugin developers make a responsible decision for whether their plugin options need to be autoloaded or not. The decision for that is usually rather straightforward to make:

  • If an option is used in logic that runs for every single WordPress request (e.g. unconditionally on the init action or wp_loaded action), the option should be autoloaded.
  • If an option is used only in certain special circumstances (e.g. only when posts of a specific post type are loaded, or only if very specific logic conditions are met, or only in WP Admin), the option should not be autoloaded.
  • A special-case is if your option is extremely large: You probably shouldn’t do that for many reasons, but if your plugin stores a complex option with an array of records with nested data that can grow more or less infinitely, it’s recommended to not autoload it, simply because of how much overhead it adds to the already large payload of the database request to autoload options. For such a large option, making a separate database request won’t do much harm relatively speaking.

Now, as a plugin developer you may intuitively think: “Why would I not autoload my plugin’s options? I want my plugin’s logic to be fast!” However, that’s a false conclusion. First of all, if your plugin’s logic only uses a single option under certain circumstances, the overhead of requesting that option from the database is probably negligible. Second, by autoloading everything without consideration, you’re setting up your users’ sites for performance problems. As plugin developers, you probably know that your plugin will not run in isolation. Hardly any WordPress site will only have your plugin active as the only plugin. So we need to handle autoloading with care, keeping the WordPress ecosystem as a whole in mind.

Of course it’s completely understandable that you don’t want to “do the right thing” at the cost of your own plugin specific logic becoming slower. But that’s the wrong way to think about it. Because even if you responsibly autoload only the options that are needed on every page load, WordPress Core provides you everything you need to also efficiently load your own options – even if you need to access several custom options at the same time.

How can you tell WordPress whether to autoload an option?

The API functionality that you need to use as a plugin developer to indicate to WordPress whether you would like an option to be autoloaded or not is quite simple: It is controlled by the $autoload boolean parameter of the add_option() function and the update_option() function respectively.

  • Semantically speaking, add_option() is the source of truth for this parameter: Whenever WordPress inserts a new option into the database, it will check whether true or false was provided for the $autoload parameter, and it will store that value alongside the option in the database. Therefore, it is crucial that you provide a value for the $autoload parameter whenever you call add_option().
  • Realistically though, many plugins never call add_option() at all. That’s because update_option() provides a convenient fall-through layer where, if called for an option that is not yet in the database, it will internally call add_option(). As such, many plugins simply call update_option(), knowing that it will do the right thing depending on whether the option is already present in the database or not. If you do that, it’s crucial that you provide a value for the $autoload parameter on update_option(), so that it gets passed through to add_option() when relevant.
    • The only time that it’s okay to not pass the parameter to update_option() is for scenarios where you know that the option will for sure already be present. That could be the case for instance if you have a separate installation routine that adds an initial value for the option to the database, or if the update_option() call is wrapped in conditional logic that will only ever run if a value for the option is already set.
    • Based on my experience with plugins I’ve seen though, probably 90+% of WordPress plugins simply call update_option() unconditionally. In any case, there’s no drawback of always providing the $autoload parameter. So if in doubt, you’re better off simply passing it.

It’s worth noting that the $autoload parameter documentation itself also briefly states the potential caveats with autoloading that I described earlier:

Autoloading too many options can lead to performance problems, especially if the options are not frequently used. For options which are accessed across several places in the frontend, it is recommended to autoload them, by using true.
For options which are accessed only on few specific URLs, it is recommended to not autoload them, by using false.

Usually, parameter descriptions in WordPress Core do not include notes about performance related concerns. But this case is an exception because of how prevalent the problem of excessive autoloading of unneeded options is.

Why excessive autoloading is a problem in practice

There’s no quantitative data on how many WordPress sites suffer from excessive option autoloading, but it’s safe to say that the problem is widespread because only a fraction of WordPress plugins in the plugin directory provide the $autoload parameter when adding their options.

And that is exactly the issue: The very root of the problem is that, back when the add_option() and update_option() functions were introduced, the $autoload parameter was made optional. This likely means many plugin developers do not even know about it. And that’s totally fair: It’s not exactly intuitive to expect that functions named add_option() and update_option() would support more parameters than just an option name and an option value. And then, even if you read the full function documentation, the $autoload parameter’s obscure name and the additional requirement of learning about option autoloading may throw people off. For what it’s worth, the documentation you find for the parameter on the add_option() function and the update_option() function today was only recently updated to what it is now. So up until last year you would have found only a brief note on it.

Last but not least, up until before WordPress 6.6, the default value for the $autoload parameter was “yes”, which meant any option where the developer wouldn’t pass an $autoload value would fall back to be autoloaded. Together with the lack of familiarity with the parameter, this led to tons of options being wastefully autoloaded.

What WordPress Core is doing to mitigate the issue

Due to the strong commitment to backward compatibility in WordPress Core, we can’t just make the $autoload parameter mandatory. But what we can do is try to come up with ways to automatically handle it in smarter ways using WordPress Core logic, and, most importantly, we can make an effort in providing more documentation and learning resources about autoloading options.

Here’s a quick summary of enhancements that were added to WordPress Core in the past year to improve the situation:

  • As of WordPress 6.4, there are a few new functions led by wp_prime_option_caches(), which allow retrieving multiple options with a single database request even if they’re not autoloaded. This is the recommended alternative for scenarios where multiple options are needed for a specific plugin’s logic, but it would not be responsible to autoload them. See the relevant WordPress developer note post and Core Trac ticket.
  • As of WordPress 6.4, there are new functions like wp_set_option_autoload_values() to modify the $autoload behavior of one or more options independently of changing the option values. This is recommended e.g. in plugin deactivation hooks, to temporarily turn off autoloading on the plugin’s options when the plugin gets deactivated. See the relevant WordPress developer note post and Core Trac ticket.
  • As of WordPress 6.6, the default value for the $autoload parameter is now null. This value means that WordPress Core will have the freedom to decide based on certain heuristics whether to autoload the value. It also more strongly encourages plugin developers to explicitly provide a suitable value themselves. See the relevant WordPress developer note post and Core Trac ticket.
    • For now, large options that exceed a certain size threshold will no longer be autoloaded, unless the developer has explicitly provided true to autoload them.
    • Performance optimization plugins can leverage a new wp_default_autoload_value filter to dynamically specify the default behavior based on their own heuristics.
    • Additional heuristics may be implemented in the future.
  • As of WordPress 6.6, there is now a Site Health check that warns about the performance implications if the total size of the site’s autoloaded options exceed a certain threshold. See the relevant WordPress developer note post and Core Trac ticket.
  • As of WordPress 6.7, all WordPress Core options explicitly set an $autoload value. For some options, this led to no longer autoloading them when they were previously being autoloaded unnecessarily. The autoload value is specified either during WordPress installation when the option is inserted, or as part of the update_option() call if the option is populated dynamically. See the relevant Core Trac ticket.

As you can see, several enhancements were made in WordPress Core that allow to better manage option autoloading. For the rest of this post, let’s take a closer look at how we can leverage these enhancements.

Recommended option autoload patterns in practice

As mentioned earlier, the most important aspect of autoloading options the right way is to consider where and how each option from your plugin is used and, based on that, decide whether it needs to be autoloaded or not. Let’s look at an example.

Imagine you write a WordPress plugin that allows small businesses to manage their data called “Small Business Contact Data” (SBCD). Let’s say the features are the following:

  • The plugin provides a WP Admin screen to centrally manage the business’s contact data (e.g. address, phone number, opening hours) as well as social media URLs.
  • It comes with a special template for a contact page for the frontend, where the contact data is displayed.
  • It also comes with a custom block or widget that shows the social media links, intended for a site’s header or footer.

To store the relevant data, we’ll use four options for this hypothetical plugin:

  • sbcd_address_data: Stores an associative array of all relevant address data.
  • sbcd_contact_data: Stores an associative array of contact information, e.g. phone numbers and email addresses.
  • sbcd_opening_hours: Stores an associative array of opening hours for different days.
  • sbcd_social_urls: Stores an associative array of social media URLs.

The sbcd_address_data, sbcd_contact_data, and sbcd_opening_hours options will only need to be loaded in two places, namely when site visitors view the contact page and when the administrator views or modifies the data in the WP Admin screen. As such, these options should not be autoloaded.

The sbcd_social_urls option will be relevant on most likely every single frontend URL of the site, as it is used to populate the social media links in the header or footer of the site. As such, this option should be autoloaded. Of course this data is also relevant when viewing or modifying the social media URLs in the WP Admin screen, but the fact that it’s needed throughout most of the frontend alone is enough to determine that this option should be autoloaded.

We can now tell WordPress about it in two different ways. Either we use our own logic to update the options when the administrator makes changes in the WP Admin screen, e.g. as part of a custom REST API endpoint: If so, we have full control as we can call update_option() ourselves, where we can pass the $autoload parameter to make sure our preference is respected as soon as the option is first inserted into the database:

update_option( 'sbcd_address_data', $address_data, false );
update_option( 'sbcd_contact_data', $contact_data, false );
update_option( 'sbcd_opening_hours', $opening_hours, false );
update_option( 'sbcd_social_urls', $social_urls, true );Code language: PHP (php)

Or alternatively we use a more traditional WordPress Settings screen using the WordPress Core Settings API. In that case, the WP Admin form submission will trigger the option update itself, so in that case WordPress Core will call update_option() and we have no control over its $autoload parameter. We can however work around it by populating an initial value for the option using add_option(), e.g. in a plugin activation hook:

register_activation_hook(
	__FILE__,
	function () {
		if ( get_option( 'sbcd_address_data' ) === false ) {
			add_option( 'sbcd_address_data', array(), '', false );
		}
		if ( get_option( 'sbcd_contact_data' ) === false ) {
			add_option( 'sbcd_contact_data', array(), '', false );
		}
		if ( get_option( 'sbcd_opening_hours' ) === false ) {
			add_option( 'sbcd_opening_hours', array(), '', false );
		}
		if ( get_option( 'sbcd_social_urls' ) === false ) {
			add_option( 'sbcd_social_urls', array(), '', true );
		}
	}
);Code language: PHP (php)

Note that the third parameter of add_option() is deprecated, that’s why we just have to pass it as an empty string here.

Loading multiple options

So far, so good. As noted previously, all data from these four options can be edited using the plugin’s WP Admin screen. Intuitively, that’s easy enough as you can simply call get_option() for each of them. While that works, this means that your plugin will trigger three separate database requests when the administrators visits your plugin’s WP Admin screen, because three out of the four options are not autoloaded.

It’s worth reiterating here that autoloading all of the options is not the right solution to this problem. If you autoloaded the four options just because you need to use all of them on this one WP Admin screen, you would contribute to the excessive autoloading problem and effectively hurt performance throughout the entire WordPress site, only to have better performance on that one WP Admin screen. So the much better choice is to use the aforementioned wp_prime_option_caches() function to load all three options at once.

Note that wp_prime_option_caches() does not actually return any values. It’s only there to load the relevant option values into memory so that no database request is needed whenever they’re subsequently accessed via get_option().

A good place to call that function is the load-{$hook_suffix} callback for your plugin’s WP Admin screen. The $hook_suffix portion of that action hook is the value that is returned from the function call that adds your custom WP Admin screen (e.g. add_menu_page(), or add_submenu_page(), or add_options_page()). By using this approach, you ensure the logic to load (or, in more technical terms “prime”) your plugin options is only run on your plugin’s own WP Admin screen. Here’s what that code could look like:

add_action(
	"load-{$hook_suffix}",
	function () {
		// This requires WordPress 6.4+.
		wp_prime_option_caches(
			array(
				'sbcd_address_data',
				'sbcd_contact_data',
				'sbcd_opening_hours',
				'sbcd_social_urls'
			)
		);
	}
);Code language: PHP (php)

Note that in the example code I’ve only included all four options, even though one of them, the sbcd_social_urls option, is autoloaded anyway. There’s no harm in doing that, as WordPress will already know that the option is autoloaded and thus is already loaded. So it won’t wastefully request it from the database. You could as well leave it out of this call, but I find it a good idea to pass it for completeness, as it is still an option that needs to be accessed on the WP Admin screen.

With this code in place, any get_option() calls for those options won’t trigger separate database requests. You have now solved the problem in a way that’s good for both the performance of your WP Admin screen and the performance of the rest of the WordPress site.

Cleaning up after yourself

The last aspect I want to focus on here is cleaning up after yourself. Naturally, WordPress site owners sometimes switch to other plugins, or they realize they don’t need a certain plugin anymore. When that happens, you want to be a good citizen of the WordPress ecosystem and clean up after yourself. In the context of performance, old plugin options that remain autoloaded are a key part of the problem as it not only leads to a lot of accumulated database bloat over time, but also gets loaded from the database on every WordPress request.

For a long time, the only way to clean up after yourself as a plugin developer was to delete the plugin’s options. However, I totally understand that sometimes this may not be desirable. For example, the administrator may later want to re-activate your plugin and if so it could be detrimental that they have lost all their preferences.

Fortunately, the wp_set_option_autoload_values() function mentioned earlier and its sibling functions allow addressing the autoloading problem without actually deleting the options. The idea is simple: Once your plugin is deactivated, you should set all your plugin’s options that should normally be autoloaded to no longer autoload, as they won’t be used again as long as your plugin is inactive. Should the site owner later re-activate your plugin, you can then reinstantiate the relevant options to be autoloaded again. For this particular purpose, the most relevant function is the wp_set_options_autoload() function, which allows to set a specific $autoload value for multiple options at the same time.

In our example plugin, the only option that should normally be autoloaded is sbcd_social_urls, so this is the only option for which we should disable autoloading upon plugin deactivation. We can do so in a plugin deactivation hook:

register_deactivation_hook(
	__FILE__,
	function () {
        	// This requires WordPress 6.4+.
		wp_set_options_autoload(
			array( 'sbcd_social_urls' ),
			false
		);
	}
);Code language: PHP (php)

Of course, as mentioned before, you wouldn’t want to remain in that state in case the site owner re-activates your plugin later. As such, you can use the plugin activation hook to re-enable autoloading for the sbcd_social_urls option:

register_activation_hook(
	__FILE__,
	function () {
        	// This requires WordPress 6.4+.
		wp_set_options_autoload(
			array( 'sbcd_social_urls' ),
			true
		);
	}
);Code language: PHP (php)

If you were to use the approach of calling add_option() to populate defaults and initial autoload values as outlined before, you could also combine the two activation hooks into one. Here’s the combined activation hook (note the end of the function for the extra condition relevant for re-activation):

register_activation_hook(
	__FILE__,
	function () {
		if ( get_option( 'sbcd_address_data' ) === false ) {
			add_option( 'sbcd_address_data', array(), '', false );
		}
		if ( get_option( 'sbcd_contact_data' ) === false ) {
			add_option( 'sbcd_contact_data', array(), '', false );
		}
		if ( get_option( 'sbcd_opening_hours' ) === false ) {
			add_option( 'sbcd_opening_hours', array(), '', false );
		}
		if ( get_option( 'sbcd_social_urls' ) === false ) {
			add_option( 'sbcd_social_urls', array(), '', true );
		} else {
			// On re-activation, set the existing option to autoload again.
			// This requires WordPress 6.4+.
			wp_set_options_autoload(
				array( 'sbcd_social_urls' ),
				true
			);
		}
	}
);Code language: PHP (php)

With each of those patterns in place, we have implemented a solid foundation for dealing with autoloading in our hypothetical plugin.

Of course a single plugin with 4 options alone won’t notably hurt performance, even if it doesn’t treat autoloading the options properly. But the combination of lots of plugins not paying attention to option autoloading is what causes this problem. Last but not least, many plugins have far more than just 4 options that they need to think about in terms of autoloading. So it is worth to think about this no matter how big or small your plugin is, to play along well with the WordPress ecosystem.

Responsibly autoloading options

I hope that this post has helped you gain a better understanding of the concept of autoloading options in WordPress, what its caveats are, and what you as a plugin developer can do to improve the situation.

As plugin developers, it’s crucial to remind ourselves that our plugins don’t run in a silo. We are part of the greater WordPress ecosystem, and we have to write our plugins with ecosystem compatibility in mind. That does not only mean that the plugin shouldn’t cause bugs in combination with another plugin, but it also means that the plugin shouldn’t notably slow down performance in combination with other plugins.

Autoloading options is a powerful performance enhancement provided by WordPress Core, but it’s easy for it to become a performance bottleneck if we don’t pay attention to it. As a plugin developer, we have to play our part in avoiding that. Please autoload your options efficiently and responsibly.


Posted

in

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *