Commit e80d7ea807 for woocommerce
commit e80d7ea807c1b1dd82f7240034eae232030e6d38
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date: Wed Feb 11 16:54:49 2026 +0100
[Performance] Recent Reviews widget: address slow SQL query (take 2) (#63224)
Re-introduced the hardened version of the widget.
diff --git a/plugins/woocommerce/changelog/perfromance-60194-optimize-recent-reviews-widget-take-2 b/plugins/woocommerce/changelog/perfromance-60194-optimize-recent-reviews-widget-take-2
new file mode 100644
index 0000000000..009cd4da41
--- /dev/null
+++ b/plugins/woocommerce/changelog/perfromance-60194-optimize-recent-reviews-widget-take-2
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Performance: reintroduced the optimized and hardened version of the recent reviews widget.
diff --git a/plugins/woocommerce/client/legacy/js/admin/wc-recent-reviews-widget-async.js b/plugins/woocommerce/client/legacy/js/admin/wc-recent-reviews-widget-async.js
new file mode 100644
index 0000000000..d0dfe92175
--- /dev/null
+++ b/plugins/woocommerce/client/legacy/js/admin/wc-recent-reviews-widget-async.js
@@ -0,0 +1,44 @@
+/**
+ * WooCommerce Recent reviews widget async loading
+ */
+jQuery(function($) {
+ 'use strict';
+
+ // Only run on admin dashboard
+ if ( ! $( '#wc-recent-reviews-widget-loading' ).length ) {
+ return;
+ }
+
+ // Load the widget content via AJAX
+ function loadRecentReviewsWidget() {
+ $.ajax({
+ url: wc_recent_reviews_widget_params.ajax_url,
+ data: {
+ action: 'woocommerce_load_recent_reviews_widget',
+ security: wc_recent_reviews_widget_params.security
+ },
+ type: 'GET',
+ dataType: 'json',
+ success: function(response) {
+ if ( response && response.success && response.data.content ) {
+ $( '#wc-recent-reviews-widget-content' ).html( response.data.content ).show();
+ $( '#wc-recent-reviews-widget-loading' ).hide();
+ } else {
+ showErrorMessage();
+ }
+ },
+ error: function() {
+ showErrorMessage();
+ }
+ });
+ }
+
+ function showErrorMessage() {
+ const message = wc_recent_reviews_widget_params.error_message || 'Error loading widget';
+ $( '#wc-recent-reviews-widget-loading' ).html( '<p>' + message + '</p>' );
+ }
+
+ // Start loading the widget after a very short delay
+ // This allows the dashboard to render quickly first
+ setTimeout( loadRecentReviewsWidget, 100 );
+});
diff --git a/plugins/woocommerce/client/legacy/js/admin/wc-status-widget-async.js b/plugins/woocommerce/client/legacy/js/admin/wc-status-widget-async.js
index 943a4ca847..b940906b99 100644
--- a/plugins/woocommerce/client/legacy/js/admin/wc-status-widget-async.js
+++ b/plugins/woocommerce/client/legacy/js/admin/wc-status-widget-async.js
@@ -3,12 +3,12 @@
*/
jQuery(function($) {
'use strict';
-
+
// Only run on admin dashboard
if (!$('#wc-status-widget-loading').length) {
return;
}
-
+
// Load the widget content via AJAX
function loadStatusWidget() {
$.ajax({
@@ -32,12 +32,13 @@ jQuery(function($) {
}
});
}
-
+
function showErrorMessage() {
- $('#wc-status-widget-loading').html('<p>' + 'Error loading widget' + '</p>');
+ const message = wc_status_widget_params.error_message || 'Error loading widget';
+ $('#wc-status-widget-loading').html('<p>' + message + '</p>');
}
-
+
// Start loading the widget after a very short delay
// This allows the dashboard to render quickly first
setTimeout(loadStatusWidget, 100);
-});
\ No newline at end of file
+});
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php
index 0556c29f67..0d1cfb1cd7 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php
@@ -143,8 +143,9 @@ if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) :
'wc-status-widget-async',
'wc_status_widget_params',
array(
- 'ajax_url' => admin_url( 'admin-ajax.php' ),
- 'security' => wp_create_nonce( 'wc-status-widget' ),
+ 'ajax_url' => admin_url( 'admin-ajax.php' ),
+ 'security' => wp_create_nonce( 'wc-status-widget' ),
+ 'error_message' => esc_html__( 'Error loading widget', 'woocommerce' ),
)
);
@@ -474,10 +475,87 @@ if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) :
}
/**
- * Recent reviews widget.
+ * Recent reviews widget: placeholder.
*/
public function recent_reviews() {
- $this->legacy_recent_reviews();
+ $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min';
+ $version = Constants::get_constant( 'WC_VERSION' );
+
+ wp_enqueue_script( 'wc-recent-reviews-widget-async', WC()->plugin_url() . '/assets/js/admin/wc-recent-reviews-widget-async' . $suffix . '.js', array( 'jquery' ), $version, true );
+ wp_localize_script(
+ 'wc-recent-reviews-widget-async',
+ 'wc_recent_reviews_widget_params',
+ array(
+ 'ajax_url' => admin_url( 'admin-ajax.php' ),
+ 'security' => wp_create_nonce( 'wc-recent-reviews-widget' ),
+ 'error_message' => esc_html__( 'Error loading widget', 'woocommerce' ),
+ )
+ );
+
+ // Display loading placeholder.
+ echo '<div id="wc-recent-reviews-widget-loading" class="wc-recent-reviews-widget-loading">';
+ echo '<p>' . esc_html__( 'Loading reviews data...', 'woocommerce' ) . ' <span class="spinner is-active"></span></p>';
+ echo '</div>';
+ echo '<div id="wc-recent-reviews-widget-content" style="display:none;"></div>';
+ }
+
+ /**
+ * Recent reviews widget: content.
+ */
+ public function recent_reviews_content(): void {
+ // Backward compatibility mode: if any of the checked below hooks are in use, use the legacy implementation.
+ $has_legacy_query_filter = has_filter( 'woocommerce_report_recent_reviews_query_from' );
+ $has_legacy_product_title_filter = has_filter( 'woocommerce_admin_dashboard_recent_reviews' );
+ $use_legacy_implementation = $has_legacy_query_filter || $has_legacy_product_title_filter;
+ if ( $use_legacy_implementation ) {
+ if ( $has_legacy_query_filter ) {
+ wc_deprecated_hook( 'woocommerce_report_recent_reviews_query_from', '10.5.0' );
+ }
+ if ( $has_legacy_product_title_filter ) {
+ wc_deprecated_hook( 'woocommerce_admin_dashboard_recent_reviews', '10.5.0', 'dashboard-widget-reviews.php template' );
+ }
+ $this->legacy_recent_reviews();
+
+ return;
+ }
+
+ // Optimized version of the widget: faster SQL queries and templates-based rendering for customization.
+ /** @var \WP_Comment[] $comments */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
+ $comments = get_comments(
+ array(
+ 'type' => 'review',
+ 'status' => 'approve',
+ 'parent' => 0,
+ 'number' => 25,
+ 'update_comment_post_cache' => true,
+ )
+ );
+ $comments = array_filter(
+ $comments,
+ static fn( \WP_Comment $comment ) => current_user_can( 'read_product', $comment->comment_post_ID ) && ! post_password_required( (int) $comment->comment_post_ID )
+ );
+ if ( $comments ) {
+ echo '<ul>';
+ $count_rendered = 0;
+ foreach ( $comments as $comment ) {
+ $product = wc_get_product( $comment->comment_post_ID );
+ if ( $product ) {
+ wc_get_template(
+ 'dashboard-widget-reviews.php',
+ array(
+ 'product' => $product,
+ 'comment' => $comment,
+ )
+ );
+ if ( 5 === ++$count_rendered ) {
+ break;
+ }
+ }
+ }
+ echo '</ul>';
+ } else {
+ echo '<p>' . esc_html__( 'There are no product reviews yet.', 'woocommerce' ) . '</p>';
+ }
}
/**
diff --git a/plugins/woocommerce/includes/class-wc-ajax.php b/plugins/woocommerce/includes/class-wc-ajax.php
index 1768c9d222..20123fb36b 100644
--- a/plugins/woocommerce/includes/class-wc-ajax.php
+++ b/plugins/woocommerce/includes/class-wc-ajax.php
@@ -215,6 +215,7 @@ class WC_AJAX {
'shipping_classes_save_changes',
'toggle_gateway_enabled',
'load_status_widget',
+ 'load_recent_reviews_widget',
);
foreach ( $ajax_events as $ajax_event ) {
@@ -3900,6 +3901,26 @@ class WC_AJAX {
wp_send_json_success( array( 'content' => $content ) );
}
+ /**
+ * AJAX handler for asynchronously loading the recent reviews widget content.
+ *
+ * @return void
+ */
+ public static function load_recent_reviews_widget() {
+ check_ajax_referer( 'wc-recent-reviews-widget', 'security' );
+
+ if ( ! current_user_can( 'publish_shop_orders' ) || ! post_type_supports( 'product', 'comments' ) ) {
+ wp_send_json_error( 'missing_permissions' );
+ }
+
+ include_once __DIR__ . '/admin/class-wc-admin-dashboard.php';
+ ob_start();
+ $wc_admin_dashboard = new WC_Admin_Dashboard();
+ $wc_admin_dashboard->recent_reviews_content();
+ $content = ob_get_clean();
+ wp_send_json_success( array( 'content' => $content ) );
+ }
+
/**
* Reimplementation of WP core's `wp_ajax_add_meta` method to support order custom meta updates with custom tables.
*
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/admin/class-wc-tests-admin-dashboard.php b/plugins/woocommerce/tests/legacy/unit-tests/admin/class-wc-tests-admin-dashboard.php
index d1069287bf..87863f5643 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/admin/class-wc-tests-admin-dashboard.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/admin/class-wc-tests-admin-dashboard.php
@@ -37,6 +37,63 @@ class WC_Tests_Admin_Dashboard extends WC_Unit_Test_Case {
remove_filter( 'woocommerce_dashboard_status_widget_reports', array( $this, 'mock_replace_dashboard_status_widget_reports' ) );
}
+ /**
+ * Test: recent reviews widget placeholder.
+ */
+ public function test_recent_reviews_widget_placeholder() {
+ wp_set_current_user( $this->user );
+ ( new WC_Admin_Dashboard() )->recent_reviews();
+ $this->expectOutputRegex( '/Loading reviews data.../' );
+ $this->expectOutputRegex( '/wc-recent-reviews-widget-loading/' );
+ $this->expectOutputRegex( '/wc-recent-reviews-widget-content/' );
+ }
+
+ /**
+ * Test: recent reviews widget content (new).
+ */
+ public function test_recent_reviews_widget_content_new_version() {
+ $product = WC_Helper_Product::create_simple_product();
+ $product_id = $product->get_id();
+ $comment_id1 = WC_Helper_Product::create_product_review( $product_id );
+ WC_Helper_Product::create_product_review( $product_id );
+ WC_Helper_Product::create_product_review( $product_id );
+ WC_Helper_Product::create_product_review( $product_id );
+ $comment_id5 = WC_Helper_Product::create_product_review( $product_id );
+
+ wp_set_current_user( $this->user );
+ ( new WC_Admin_Dashboard() )->recent_reviews_content();
+ $this->expectOutputRegex( "/#comment-{$comment_id1}/" );
+ $this->expectOutputRegex( "/#comment-{$comment_id5}/" );
+ $this->expectOutputRegex( '/reviewed by/' );
+
+ $product->delete();
+ }
+
+ /**
+ * Test: recent reviews widget content (legacy).
+ */
+ public function test_recent_reviews_widget_content_legacy_version() {
+ add_filter( 'woocommerce_report_recent_reviews_query_from', $legacy_filter = fn( string $sql ) => $sql );
+ $this->expected_deprecated = array_merge( $this->expected_deprecated, array( 'woocommerce_report_recent_reviews_query_from' ) );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $product_id = $product->get_id();
+ $comment_id1 = WC_Helper_Product::create_product_review( $product_id );
+ WC_Helper_Product::create_product_review( $product_id );
+ WC_Helper_Product::create_product_review( $product_id );
+ WC_Helper_Product::create_product_review( $product_id );
+ $comment_id5 = WC_Helper_Product::create_product_review( $product_id );
+
+ wp_set_current_user( $this->user );
+ ( new WC_Admin_Dashboard() )->recent_reviews_content();
+ $this->expectOutputRegex( "/#comment-{$comment_id1}/" );
+ $this->expectOutputRegex( "/#comment-{$comment_id5}/" );
+ $this->expectOutputRegex( '/reviewed by/' );
+
+ $product->delete();
+ remove_filter( 'woocommerce_report_recent_reviews_query_from', $legacy_filter );
+ }
+
/**
* Test: status_widget placeholder
*/