When working with certain WordPress hooks, you may notice that they are triggered multiple times during a single request. For example, the save_post hook can be fired several times when saving a post with metadata or revisions. If you have time-consuming operations (like sending emails or calling external APIs) attached to these hooks, it can severely degrade site performance. In this article, we’ll explore two effective methods to optimize these operations.
Performance Issues with Repeated Hooks
A classic example of a repeated hook is woocommerce_update_order. This hook is often triggered multiple times when an order is created or updated in WooCommerce. Consider the following code:
add_action( 'woocommerce_update_order', function( $order_id ) {
$email = 'your-email@example.com';
wp_mail( $email, 'Test Email', sprintf( 'Order %d has been updated', $order_id ) );
} );
After adding this code, updating an order might result in a flood of emails in your inbox. Since sending emails is a slow operation involving SMTP server connections, this significantly slows down the order update process for the user. Let’s look at how to fix this.
Method 1: Removing the Hook During Execution
The simplest way to ensure a function only runs once per request is to remove the action from the hook within the function itself. This prevents subsequent triggers from executing the same logic.
add_action( 'woocommerce_update_order', 'wprs_order_update_once', 25, 2 );
function wprs_order_update_once( $order_id, $order ) {
// Remove the action immediately so it doesn't run again in this request
remove_action( 'woocommerce_update_order', __FUNCTION__, 25, 2 );
// Perform your time-consuming operation here
}
This method is straightforward but has a limitation: if WooCommerce performs crucial updates in later triggers that your function needs to know about, you might miss them because you disconnected after the first trigger.
Method 2: Using Scheduled Asynchronous Tasks
A more robust approach is to offload the time-consuming task to a background process. WooCommerce includes an “Action Scheduler” library that is perfect for this. Instead of running the task immediately, we schedule it to run once, slightly in the future.
add_action( 'woocommerce_update_order', 'wprs_maybe_schedule_order_update' );
function wprs_maybe_schedule_order_update( $order_id ) {
// Schedule a single action to run 5 seconds from now
as_schedule_single_action(
time() + 5,
'wprs_deferred_order_update',
array( $order_id ),
'my_custom_group',
true // Ensure the action is unique for this order
);
}
add_action( 'wprs_deferred_order_update', function( $order_id ) {
$order = wc_get_order( $order_id );
// Perform the heavy lifting here (e.g., calling an external API)
} );
By setting the final parameter of as_schedule_single_action to true, we ensure that only one instance of this task is scheduled for the given order, regardless of how many times the trigger hook fires. This approach provides a much smoother user experience while ensuring data integrity.
For a related discussion of repeated hooks causing recursive execution, see Avoid Infinite Loops When Developing WordPress Themes with Hooks. If you want a more general queue-based approach beyond WooCommerce, Use WP Queue in WordPress to Run Time-Consuming Tasks Through a Job Queue is also worth reading.
