Load controllers only when needed for performance. (#47704)

* Load controllers only when needed for performance.

* Classify controllers based on their namespace and load selectively.

* Enable private namespace along with store api.

* Only prevent route loading when request is known for back compat.

* Lint fixes.

* Remove duplicate inclusion.

* Correctly load feature controller.

* Add since tag.

* Add unit tests.
This commit is contained in:
Vedanshu Jain 2024-07-09 14:37:14 +05:30 committed by GitHub
parent fcb7f6798f
commit 6d92aa9ccb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 189 additions and 75 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: performance
Load REST API namespaces only when needed.

View File

@ -28,7 +28,7 @@ class Server {
/** /**
* Hook into WordPress ready to init the REST API as needed. * Hook into WordPress ready to init the REST API as needed.
*/ */
public function init() { public function init() { // phpcs:ignore WooCommerce.Functions.InternalInjectionMethod -- Not an injection method.
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ), 10 ); add_action( 'rest_api_init', array( $this, 'register_rest_routes' ), 10 );
\WC_REST_System_Status_V2_Controller::register_cache_clean(); \WC_REST_System_Status_V2_Controller::register_cache_clean();
@ -57,13 +57,19 @@ class Server {
* @return array List of Namespaces and Main controller classes. * @return array List of Namespaces and Main controller classes.
*/ */
protected function get_rest_namespaces() { protected function get_rest_namespaces() {
/**
* Filter the list of REST API controllers to load.
*
* @since 4.5.0
* @param array $controllers List of $namespace => $controllers to load.
*/
return apply_filters( return apply_filters(
'woocommerce_rest_api_get_rest_namespaces', 'woocommerce_rest_api_get_rest_namespaces',
array( array(
'wc/v1' => $this->get_v1_controllers(), 'wc/v1' => wc_rest_should_load_namespace( 'wc/v1' ) ? $this->get_v1_controllers() : array(),
'wc/v2' => $this->get_v2_controllers(), 'wc/v2' => wc_rest_should_load_namespace( 'wc/v2' ) ? $this->get_v2_controllers() : array(),
'wc/v3' => $this->get_v3_controllers(), 'wc/v3' => wc_rest_should_load_namespace( 'wc/v3' ) ? $this->get_v3_controllers() : array(),
'wc-telemetry' => $this->get_telemetry_controllers(), 'wc-telemetry' => wc_rest_should_load_namespace( 'wc-telemetry' ) ? $this->get_telemetry_controllers() : array(),
) )
); );
} }

View File

@ -373,3 +373,56 @@ function wc_rest_check_product_reviews_permissions( $context = 'read', $object_i
function wc_rest_is_from_product_editor() { function wc_rest_is_from_product_editor() {
return isset( $_SERVER['HTTP_X_WC_FROM_PRODUCT_EDITOR'] ) && '1' === $_SERVER['HTTP_X_WC_FROM_PRODUCT_EDITOR']; return isset( $_SERVER['HTTP_X_WC_FROM_PRODUCT_EDITOR'] ) && '1' === $_SERVER['HTTP_X_WC_FROM_PRODUCT_EDITOR'];
} }
/**
* Check if a REST namespace should be loaded. Useful to maintain site performance even when lots of REST namespaces are registered.
*
* @since 9.2.0.
*
* @param string $ns The namespace to check.
* @param string $rest_route (Optional) The REST route being checked.
*
* @return bool True if the namespace should be loaded, false otherwise.
*/
function wc_rest_should_load_namespace( string $ns, string $rest_route = '' ): bool {
if ( '' === $rest_route ) {
$rest_route = $GLOBALS['wp']->query_vars['rest_route'] ?? '';
}
if ( '' === $rest_route ) {
return true;
}
$rest_route = trailingslashit( ltrim( $rest_route, '/' ) );
$ns = trailingslashit( $ns );
/**
* Known namespaces that we know are safe to not load if the request is not for them. Namespaces not in this namespace should always be loaded, because we don't know if they won't be making another internal REST request to an unloaded namespace.
*/
$known_namespaces = array(
'wc/v1',
'wc/v2',
'wc/v3',
'wc-telemetry',
'wc-admin',
'wc-analytics',
'wc/store',
'wc/private',
);
// We can consider allowing filtering this list in the future.
$known_namespace_request = false;
foreach ( $known_namespaces as $known_namespace ) {
if ( str_starts_with( $rest_route, $known_namespace ) ) {
$known_namespace_request = true;
break;
}
}
if ( ! $known_namespace_request ) {
return true;
}
return str_starts_with( $rest_route, $ns );
}

View File

@ -57,14 +57,14 @@ class Init {
* Init REST API. * Init REST API.
*/ */
public function rest_api_init() { public function rest_api_init() {
$controllers = array();
$analytics_controllers = array();
if ( wc_rest_should_load_namespace( 'wc-admin' ) ) {
// Controllers in the wc-admin namespace.
$controllers = array( $controllers = array(
'Automattic\WooCommerce\Admin\API\Notice',
'Automattic\WooCommerce\Admin\API\Features', 'Automattic\WooCommerce\Admin\API\Features',
'Automattic\WooCommerce\Admin\API\Notes',
'Automattic\WooCommerce\Admin\API\NoteActions',
'Automattic\WooCommerce\Admin\API\Coupons',
'Automattic\WooCommerce\Admin\API\Data',
'Automattic\WooCommerce\Admin\API\DataCountries',
'Automattic\WooCommerce\Admin\API\DataDownloadIPs',
'Automattic\WooCommerce\Admin\API\Experiments', 'Automattic\WooCommerce\Admin\API\Experiments',
'Automattic\WooCommerce\Admin\API\Marketing', 'Automattic\WooCommerce\Admin\API\Marketing',
'Automattic\WooCommerce\Admin\API\MarketingOverview', 'Automattic\WooCommerce\Admin\API\MarketingOverview',
@ -72,19 +72,8 @@ class Init {
'Automattic\WooCommerce\Admin\API\MarketingChannels', 'Automattic\WooCommerce\Admin\API\MarketingChannels',
'Automattic\WooCommerce\Admin\API\MarketingCampaigns', 'Automattic\WooCommerce\Admin\API\MarketingCampaigns',
'Automattic\WooCommerce\Admin\API\MarketingCampaignTypes', 'Automattic\WooCommerce\Admin\API\MarketingCampaignTypes',
'Automattic\WooCommerce\Admin\API\Notice',
'Automattic\WooCommerce\Admin\API\Options', 'Automattic\WooCommerce\Admin\API\Options',
'Automattic\WooCommerce\Admin\API\Orders',
'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions', 'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions',
'Automattic\WooCommerce\Admin\API\Products',
'Automattic\WooCommerce\Admin\API\ProductAttributes',
'Automattic\WooCommerce\Admin\API\ProductAttributeTerms',
'Automattic\WooCommerce\Admin\API\ProductCategories',
'Automattic\WooCommerce\Admin\API\ProductVariations',
'Automattic\WooCommerce\Admin\API\ProductReviews',
'Automattic\WooCommerce\Admin\API\ProductVariations',
'Automattic\WooCommerce\Admin\API\ProductsLowInStock',
'Automattic\WooCommerce\Admin\API\SettingOptions',
'Automattic\WooCommerce\Admin\API\Themes', 'Automattic\WooCommerce\Admin\API\Themes',
'Automattic\WooCommerce\Admin\API\Plugins', 'Automattic\WooCommerce\Admin\API\Plugins',
'Automattic\WooCommerce\Admin\API\OnboardingFreeExtensions', 'Automattic\WooCommerce\Admin\API\OnboardingFreeExtensions',
@ -95,15 +84,37 @@ class Init {
'Automattic\WooCommerce\Admin\API\OnboardingPlugins', 'Automattic\WooCommerce\Admin\API\OnboardingPlugins',
'Automattic\WooCommerce\Admin\API\OnboardingProducts', 'Automattic\WooCommerce\Admin\API\OnboardingProducts',
'Automattic\WooCommerce\Admin\API\NavigationFavorites', 'Automattic\WooCommerce\Admin\API\NavigationFavorites',
'Automattic\WooCommerce\Admin\API\Taxes',
'Automattic\WooCommerce\Admin\API\MobileAppMagicLink', 'Automattic\WooCommerce\Admin\API\MobileAppMagicLink',
'Automattic\WooCommerce\Admin\API\ShippingPartnerSuggestions', 'Automattic\WooCommerce\Admin\API\ShippingPartnerSuggestions',
); );
}
if ( Features::is_enabled( 'launch-your-store' ) ) { if ( Features::is_enabled( 'launch-your-store' ) ) {
$controllers[] = 'Automattic\WooCommerce\Admin\API\LaunchYourStore'; $controllers[] = 'Automattic\WooCommerce\Admin\API\LaunchYourStore';
} }
if ( wc_rest_should_load_namespace( 'wc-analytics' ) ) {
// Controllers in wc-analytics namespace, but loaded irrespective of analytics feature value.
$analytic_mu_controllers = array(
'Automattic\WooCommerce\Admin\API\Notes',
'Automattic\WooCommerce\Admin\API\NoteActions',
'Automattic\WooCommerce\Admin\API\Coupons',
'Automattic\WooCommerce\Admin\API\Data',
'Automattic\WooCommerce\Admin\API\DataCountries',
'Automattic\WooCommerce\Admin\API\DataDownloadIPs',
'Automattic\WooCommerce\Admin\API\Orders',
'Automattic\WooCommerce\Admin\API\Products',
'Automattic\WooCommerce\Admin\API\ProductAttributes',
'Automattic\WooCommerce\Admin\API\ProductAttributeTerms',
'Automattic\WooCommerce\Admin\API\ProductCategories',
'Automattic\WooCommerce\Admin\API\ProductVariations',
'Automattic\WooCommerce\Admin\API\ProductReviews',
'Automattic\WooCommerce\Admin\API\ProductVariations',
'Automattic\WooCommerce\Admin\API\ProductsLowInStock',
'Automattic\WooCommerce\Admin\API\SettingOptions',
'Automattic\WooCommerce\Admin\API\Taxes',
);
if ( Features::is_enabled( 'analytics' ) ) { if ( Features::is_enabled( 'analytics' ) ) {
$analytics_controllers = array( $analytics_controllers = array(
'Automattic\WooCommerce\Admin\API\Customers', 'Automattic\WooCommerce\Admin\API\Customers',
@ -131,9 +142,13 @@ class Init {
'Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\Controller', 'Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\Controller',
); );
// The performance indicators controller must be registered last, after other /stats endpoints have been registered. // The performance indicators controllerq must be registered last, after other /stats endpoints have been registered.
$analytics_controllers[] = 'Automattic\WooCommerce\Admin\API\Reports\PerformanceIndicators\Controller'; $analytics_controllers[] = 'Automattic\WooCommerce\Admin\API\Reports\PerformanceIndicators\Controller';
$controllers = array_merge( $controllers, $analytics_controllers );
$analytics_controllers = array_merge( $analytics_controllers, $analytic_mu_controllers );
}
$controllers = array_merge( $controllers, $analytics_controllers, $analytic_mu_controllers );
} }
/** /**

View File

@ -23,6 +23,9 @@ final class StoreApi {
add_action( add_action(
'rest_api_init', 'rest_api_init',
function () { function () {
if ( ! wc_rest_should_load_namespace( 'wc/store' ) && ! wc_rest_should_load_namespace( 'wc/private' ) ) {
return;
}
self::container()->get( Legacy::class )->init(); self::container()->get( Legacy::class )->init();
self::container()->get( RoutesController::class )->register_all_routes(); self::container()->get( RoutesController::class )->register_all_routes();
} }
@ -31,6 +34,9 @@ final class StoreApi {
add_action( add_action(
'rest_api_init', 'rest_api_init',
function () { function () {
if ( ! wc_rest_should_load_namespace( 'wc/store' ) ) {
return;
}
self::container()->get( Authentication::class )->init(); self::container()->get( Authentication::class )->init();
}, },
11 11

View File

@ -0,0 +1,30 @@
<?php
declare( strict_types = 1);
// phpcs:disable Squiz.Classes.ClassFileName.NoMatch -- backcompat nomenclature.
/**
* Tests for wc-rest-functions.php.
* Class WC_Rest_Functions_Test.
*/
class WCRestFunctionsTest extends WC_Unit_Test_Case {
/**
* @testDox All namespaces are loaded for unknown path.
*/
public function test_wc_rest_should_load_namespace_unknown() {
$this->assertTrue( wc_rest_should_load_namespace( 'wc/v1', 'wc/unknown' ) );
$this->assertTrue( wc_rest_should_load_namespace( 'wc-analytics', 'wc/unknown' ) );
$this->assertTrue( wc_rest_should_load_namespace( 'wc-telemetry', 'wc/unknown' ) );
$this->assertTrue( wc_rest_should_load_namespace( 'wc-random', 'wc/unknown' ) );
}
/**
* @testDox Only required namespace is loaded for known path.
*/
public function test_wc_rest_should_load_namespace_known() {
$this->assertFalse( wc_rest_should_load_namespace( 'wc/v1', 'wc/v2' ) );
$this->assertFalse( wc_rest_should_load_namespace( 'wc-analytics', 'wc/v2' ) );
$this->assertTrue( wc_rest_should_load_namespace( 'wc/v2', 'wc/v2' ) );
}
}