Commit 67ec574389b for woocommerce

commit 67ec574389bc5c67443fd15fa73b7a8b447b8edb
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date:   Wed Nov 11 08:56:55 2026 +0100

    [Performance] Introduce lazy loading block registration (inner blocks) (#70735)

    This PR introduces lazy loading for cart/mini-cart/checkout inner blocks in order to speedup block initialization (which is the biggest timing block in Woo bootstrap sequence).

diff --git a/plugins/woocommerce/changelog/performance-optimize-blocks-registration b/plugins/woocommerce/changelog/performance-optimize-blocks-registration
new file mode 100644
index 00000000000..6e902bb959d
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-optimize-blocks-registration
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Introduce lazy block registration for inner blocks.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.json
index 525a2ab3711..fe099e1efcc 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.json
@@ -45,6 +45,17 @@
 			"type": "string"
 		}
 	},
+	"styles": [
+		{
+			"name": "without-price",
+			"label": "Hide Price",
+			"isDefault": true
+		},
+		{
+			"name": "with-price",
+			"label": "Show Price"
+		}
+	],
 	"parent": [ "woocommerce/checkout-fields-block" ],
 	"textdomain": "woocommerce",
 	"$schema": "https://schemas.wp.org/trunk/block.json",
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 97652e2e1e3..601b89d2225 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -47823,12 +47823,6 @@ parameters:
 			count: 1
 			path: src/Blocks/BlockTypes/Checkout.php

-		-
-			message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypes\\CheckoutActionsBlock\:\:initialize\(\) has no return type specified\.$#'
-			identifier: missingType.return
-			count: 1
-			path: src/Blocks/BlockTypes/CheckoutActionsBlock.php
-
 		-
 			message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypes\\CheckoutActionsBlock\:\:register_style_variations\(\) has no return type specified\.$#'
 			identifier: missingType.return
@@ -50385,12 +50379,6 @@ parameters:
 			count: 1
 			path: src/Blocks/BlockTypesController.php

-		-
-			message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypesController\:\:register_blocks\(\) has no return type specified\.$#'
-			identifier: missingType.return
-			count: 1
-			path: src/Blocks/BlockTypesController.php
-
 		-
 			message: '#^Parameter \#1 \$str of function strtolower expects string, string\|null given\.$#'
 			identifier: argument.type
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php
index 42383139b01..244e274ea44 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php
@@ -145,8 +145,14 @@ abstract class AbstractBlock {
 			_doing_it_wrong( __METHOD__, esc_html( 'Block name is required.' ), '4.5.0' );
 			return false;
 		}
+
 		$this->integration_registry->initialize( $this->block_name . '_block' );
-		$this->register_block_type();
+
+		// Performance note: if the block is lazily loadable, it's type has been already registered.
+		if ( ! \WP_Block_Type_Registry::get_instance()->is_registered( $this->get_full_block_name() ) ) {
+			$this->register_block_type();
+		}
+
 		add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] );
 	}

diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutActionsBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutActionsBlock.php
index 11511eaa9ba..f2ee53a3e4e 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutActionsBlock.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutActionsBlock.php
@@ -15,35 +15,23 @@ class CheckoutActionsBlock extends AbstractInnerBlock {
 	/**
 	 * Initialize this block type.
 	 *
-	 * - Hook into WP lifecycle.
-	 * - Register the block with WordPress.
+	 * @return void
 	 */
 	protected function initialize() {
 		parent::initialize();

-		add_action( 'wp_loaded', array( $this, 'register_style_variations' ) );
+		// Backward compatibility: keep hook for child classes (third-parties which might be overriding register_style_variations).
+		if ( static::class !== __CLASS__ ) {
+			add_action( 'wp_loaded', array( $this, 'register_style_variations' ) );
+		}
 	}

 	/**
 	 * Register style variations for the block.
+	 *
+	 * @internal
+	 * @deprecated 11.4.0 Style variations are now declared in block.json.
 	 */
 	public function register_style_variations() {
-		register_block_style(
-			$this->get_full_block_name(),
-			array(
-				'name'       => 'without-price',
-				'label'      => __( 'Hide Price', 'woocommerce' ),
-				'is_default' => true,
-			)
-		);
-
-		register_block_style(
-			$this->get_full_block_name(),
-			array(
-				'name'       => 'with-price',
-				'label'      => __( 'Show Price', 'woocommerce' ),
-				'is_default' => false,
-			)
-		);
 	}
 }
diff --git a/plugins/woocommerce/src/Blocks/BlockTypesController.php b/plugins/woocommerce/src/Blocks/BlockTypesController.php
index b2d6e94131b..32da67eabca 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypesController.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypesController.php
@@ -46,6 +46,13 @@ final class BlockTypesController {
 	 */
 	protected $asset_data_registry;

+	/**
+	 * Controller for lazy-loaded blocks.
+	 *
+	 * @var BlockTypesLazyController
+	 */
+	private $lazy_blocks_controller;
+
 	/**
 	 * Holds the registered blocks that have WooCommerce blocks as their parents.
 	 *
@@ -68,12 +75,15 @@ final class BlockTypesController {
 	/**
 	 * Constructor.
 	 *
-	 * @param AssetApi          $asset_api Instance of the asset API.
+	 * @param AssetApi          $asset_api           Instance of the asset API.
 	 * @param AssetDataRegistry $asset_data_registry Instance of the asset data registry.
 	 */
 	public function __construct( AssetApi $asset_api, AssetDataRegistry $asset_data_registry ) {
 		$this->asset_api           = $asset_api;
 		$this->asset_data_registry = $asset_data_registry;
+		// The lazy controller is not extensible as we are in rolling migration phase, hence no constructor injection.
+		$this->lazy_blocks_controller = new BlockTypesLazyController( $asset_api, $asset_data_registry );
+
 		$this->init();
 	}

@@ -88,11 +98,15 @@ final class BlockTypesController {
 		add_action( 'woocommerce_login_form_end', array( $this, 'redirect_to_field' ) );
 		add_action( 'woocommerce_register_form_end', array( $this, 'redirect_to_field' ) );
 		add_filter( 'widget_types_to_hide_from_legacy_widget_block', array( $this, 'hide_legacy_widgets_with_block_equivalent' ) );
-		add_filter( 'block_type_metadata', array( $this, 'handle_block_type_metadata' ) );
-		add_filter( 'block_type_metadata_settings', array( $this, 'use_single_block_editor_style' ), 10, 2 );
 		add_filter( 'register_block_type_args', array( $this, 'enqueue_block_style_for_classic_themes' ), 10, 2 );
 		add_filter( 'block_core_breadcrumbs_post_type_settings', array( $this, 'set_product_breadcrumbs_preferred_taxonomy' ), 10, 3 );
 		add_filter( 'block_core_breadcrumbs_items', array( $this, 'apply_woocommerce_breadcrumb_filters' ), 10, 1 );
+
+		// Hooks relevant to '\register_block_type_from_metadata' (blocks registration workflows).
+		add_filter( 'block_type_metadata', array( $this, 'handle_block_type_metadata' ) );
+		if ( is_admin() ) {
+			add_filter( 'block_type_metadata_settings', array( $this, 'use_single_block_editor_style' ), 10, 2 );
+		}
 	}

 	/**
@@ -136,6 +150,8 @@ final class BlockTypesController {

 	/**
 	 * Register blocks, hooking up assets and render functions as needed.
+	 *
+	 * @return void
 	 */
 	public function register_blocks() {
 		if ( self::$register_blocks_has_run ) {
@@ -145,11 +161,10 @@ final class BlockTypesController {
 		// Set before registering rather than after, so a registration failure is not retried on a later call.
 		self::$register_blocks_has_run = true;
 		$this->register_block_metadata();
-		$block_types = $this->get_block_types();
-
-		foreach ( $block_types as $block_type ) {
-			$block_type_class = __NAMESPACE__ . '\\BlockTypes\\' . $block_type;

+		$block_classes = array_map( static fn( $type ) => __NAMESPACE__ . '\\BlockTypes\\' . $type, $this->get_block_types() );
+		$block_classes = array_diff( $block_classes, $this->lazy_blocks_controller->register_blocks( $block_classes ) );
+		foreach ( $block_classes as $block_type_class ) {
 			new $block_type_class( $this->asset_api, $this->asset_data_registry, new IntegrationRegistry() );
 		}
 	}
@@ -388,6 +403,9 @@ final class BlockTypesController {
 		 * @param array $allowed_namespaces List of namespaces.
 		 */
 		$allowed_namespaces = array_merge( array( 'woocommerce', 'woocommerce-checkout' ), (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_namespace', array() ) );
+		if ( in_array( $block_namespace, $allowed_namespaces, true ) ) {
+			return true;
+		}

 		/**
 		 * Filters the list of allowed Block Names
@@ -399,13 +417,11 @@ final class BlockTypesController {
 		 * @param array $allowed_namespaces List of namespaces.
 		 */
 		$allowed_blocks = (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_block', array() );
+		if ( ! empty( $allowed_blocks ) && in_array( $block_name, $allowed_blocks, true ) ) {
+			return true;
+		}

-		$blocks_with_woo_parents   = $this->get_registered_blocks_with_woocommerce_parent();
-		$block_has_woo_parent      = in_array( $block_name, array_keys( $blocks_with_woo_parents ), true );
-		$in_allowed_namespace_list = in_array( $block_namespace, $allowed_namespaces, true );
-		$in_allowed_block_list     = in_array( $block_name, $allowed_blocks, true );
-
-		return $block_has_woo_parent || $in_allowed_block_list || $in_allowed_namespace_list;
+		return ! empty( $block_name ) && isset( $this->get_registered_blocks_with_woocommerce_parent()[ $block_name ] );
 	}

 	/**
@@ -416,7 +432,6 @@ final class BlockTypesController {
 	 * @return string
 	 */
 	public function add_data_attributes( $content, $block ) {
-
 		if ( ! is_string( $content ) || ! $this->block_should_have_data_attributes( $block['blockName'] ) ) {
 			return $content;
 		}
@@ -680,9 +695,9 @@ final class BlockTypesController {
 		if ( null === $should_enqueue_block_style_for_classic_themes ) {
 			$should_enqueue_block_style_for_classic_themes = ! (
 				is_admin() ||
-				wp_is_block_theme() ||
-				( function_exists( 'wp_should_load_block_assets_on_demand' ) && wp_should_load_block_assets_on_demand() ) ||
-				wp_should_load_separate_core_block_assets()
+				wp_should_load_separate_core_block_assets() ||
+				wp_should_load_block_assets_on_demand() ||
+				wp_is_block_theme()
 			);
 		}
 		if ( ! $should_enqueue_block_style_for_classic_themes ) {
@@ -728,16 +743,10 @@ final class BlockTypesController {
 	 * @return array Block metadata.
 	 */
 	public function handle_block_type_metadata( $metadata ) {
-		if ( ! is_array( $metadata ) || ! $this->is_woocommerce_block_metadata( $metadata ) ) {
-			return $metadata;
-		}
-
-		if ( isset( $metadata['version'] ) && '' !== $metadata['version'] ) {
-			return $metadata;
+		if ( is_array( $metadata ) && '' === ( $metadata['version'] ?? '' ) && $this->is_woocommerce_block_metadata( $metadata ) ) {
+			$metadata['version'] = $this->asset_api->wc_version;
 		}

-		$metadata['version'] = $this->asset_api->wc_version;
-
 		return $metadata;
 	}

@@ -756,18 +765,13 @@ final class BlockTypesController {
 	 * @return array Block settings.
 	 */
 	public function use_single_block_editor_style( $settings, $metadata ) {
-		if (
-			! BlockEditorUnifiedAssets::is_enabled() ||
-			! is_admin() ||
-			! $this->is_woocommerce_block_metadata( $metadata ) ) {
-			return $settings;
+		if ( $this->is_woocommerce_block_metadata( $metadata ) && BlockEditorUnifiedAssets::is_enabled() ) {
+			$settings['style_handles']        = array();
+			$settings['style']                = array();
+			$settings['editor_style_handles'] = array( 'wc-block-library-style' );
+			$settings['editor_style']         = array( 'wc-block-library-style' );
 		}

-		$settings['style_handles']        = array();
-		$settings['style']                = array();
-		$settings['editor_style_handles'] = array( 'wc-block-library-style' );
-		$settings['editor_style']         = array( 'wc-block-library-style' );
-
 		return $settings;
 	}

@@ -780,22 +784,16 @@ final class BlockTypesController {
 	 */
 	private function is_woocommerce_block_metadata( $metadata ) {
 		static $blocks_path = null;
-
 		if ( null === $blocks_path ) {
 			$resolved_path = realpath( WC_ABSPATH . 'assets/client/blocks' );
-			$blocks_path   = false === $resolved_path
-				? ''
-				: trailingslashit( wp_normalize_path( $resolved_path ) );
+			$blocks_path   = false === $resolved_path ? '' : trailingslashit( wp_normalize_path( $resolved_path ) );
 		}

-		if ( '' === $blocks_path || empty( $metadata['file'] ) ) {
-			return false;
+		if ( '' !== $blocks_path && ! empty( $metadata['file'] ) ) {
+			return str_starts_with( wp_normalize_path( $metadata['file'] ), $blocks_path );
 		}

-		return str_starts_with(
-			wp_normalize_path( $metadata['file'] ),
-			$blocks_path
-		);
+		return false;
 	}

 	/**
diff --git a/plugins/woocommerce/src/Blocks/BlockTypesLazyController.php b/plugins/woocommerce/src/Blocks/BlockTypesLazyController.php
new file mode 100644
index 00000000000..24018a2de37
--- /dev/null
+++ b/plugins/woocommerce/src/Blocks/BlockTypesLazyController.php
@@ -0,0 +1,188 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Blocks;
+
+use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
+use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
+use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
+use Automattic\WooCommerce\Internal\Features\BlockEditorUnifiedAssets;
+
+/**
+ * Registers side-effect-free blocks deferring class instantiation to render time.
+ *
+ * @internal
+ */
+final class BlockTypesLazyController {
+	/**
+	 * Instance of the asset API.
+	 *
+	 * @var AssetApi
+	 */
+	private AssetApi $asset_api;
+
+	/**
+	 * Instance of the asset data registry.
+	 *
+	 * @var AssetDataRegistry
+	 */
+	private AssetDataRegistry $asset_data_registry;
+
+	/**
+	 * Processed block classes registry (class FQN → full block name).
+	 *
+	 * @var array<string,string>
+	 */
+	private array $registered_blocks = array();
+
+	/**
+	 * Constructor.
+	 *
+	 * @param AssetApi          $asset_api           Instance of the asset API.
+	 * @param AssetDataRegistry $asset_data_registry Instance of the asset data registry.
+	 */
+	public function __construct( AssetApi $asset_api, AssetDataRegistry $asset_data_registry ) {
+		$this->asset_api           = $asset_api;
+		$this->asset_data_registry = $asset_data_registry;
+	}
+
+	/**
+	 * Register side-effect-free blocks (class instantiation deferred to render time).
+	 *
+	 * @param string[] $whitelist Block class FQNs intended for initialization.
+	 * @return string[]
+	 */
+	public function register_blocks( array $whitelist ): array {
+		if ( ! empty( $this->registered_blocks ) ) {
+			return array_keys( array_filter( $this->registered_blocks, array( \WP_Block_Type_Registry::get_instance(), 'is_registered' ) ) );
+		}
+
+		$whitelisted           = array_flip( $whitelist );
+		$unified_editor_assets = BlockEditorUnifiedAssets::is_enabled();
+
+		$inner_block_groups = array(
+			$this->get_cart_inner_blocks(),
+			$this->get_mini_cart_inner_blocks(),
+			$this->get_checkout_inner_blocks(),
+		);
+
+		foreach ( $inner_block_groups as $blocks_group ) {
+			foreach ( $blocks_group as $block_name => $block_class ) {
+				if ( isset( $whitelisted[ $block_class ] ) ) {
+					$registered_block_type = register_block_type_from_metadata(
+						$this->asset_api->generate_block_metadata_path( $block_name, 'inner-blocks/' ),
+						array(
+							// Please, don't extend this list further! We expect everyone to lean onto block.json.
+							'render_callback' => $this->create_lazy_render_callback( $block_class ),
+							'editor_style'    => $unified_editor_assets ? 'wc-block-library-style' : 'wc-blocks-editor-style',
+						)
+					);
+					if ( $registered_block_type instanceof \WP_Block_Type ) {
+						$this->registered_blocks[ $block_class ] = $registered_block_type->name;
+					}
+				}
+			}
+		}
+
+		return array_keys( $this->registered_blocks );
+	}
+
+	/**
+	 * Create a render callback that lazily instantiates the block class on it's first render.
+	 *
+	 * @param string $block_type_class Fully-qualified class name.
+	 * @return callable
+	 */
+	private function create_lazy_render_callback( string $block_type_class ): callable {
+		$asset_api           = $this->asset_api;
+		$asset_data_registry = $this->asset_data_registry;
+
+		return static function ( $attributes = array(), $content = '', $block = null ) use ( $block_type_class, $asset_api, $asset_data_registry ) {
+			static $instance = null;
+			$instance        = $instance ?? new $block_type_class( $asset_api, $asset_data_registry, new IntegrationRegistry() );
+			return $instance->render_callback( $attributes, $content, $block );
+		};
+	}
+
+	/**
+	 * Side effect free and stateless Cart inner blocks.
+	 *
+	 * @return array<string,string>
+	 */
+	private function get_cart_inner_blocks(): array {
+		// ProceedToCheckoutBlock: excluded — overrides enqueue_data() with editor-visible checkout page URL.
+		return array(
+			'cart-accepted-payment-methods-block'  => \Automattic\WooCommerce\Blocks\BlockTypes\CartAcceptedPaymentMethodsBlock::class,
+			'cart-cross-sells-block'               => \Automattic\WooCommerce\Blocks\BlockTypes\CartCrossSellsBlock::class,
+			'cart-cross-sells-products-block'      => \Automattic\WooCommerce\Blocks\BlockTypes\CartCrossSellsProductsBlock::class,
+			'cart-express-payment-block'           => \Automattic\WooCommerce\Blocks\BlockTypes\CartExpressPaymentBlock::class,
+			'cart-items-block'                     => \Automattic\WooCommerce\Blocks\BlockTypes\CartItemsBlock::class,
+			'cart-line-items-block'                => \Automattic\WooCommerce\Blocks\BlockTypes\CartLineItemsBlock::class,
+			'cart-order-summary-block'             => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryBlock::class,
+			'cart-order-summary-coupon-form-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryCouponFormBlock::class,
+			'cart-order-summary-discount-block'    => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryDiscountBlock::class,
+			'cart-order-summary-fee-block'         => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryFeeBlock::class,
+			'cart-order-summary-heading-block'     => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryHeadingBlock::class,
+			'cart-order-summary-shipping-block'    => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryShippingBlock::class,
+			'cart-order-summary-subtotal-block'    => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummarySubtotalBlock::class,
+			'cart-order-summary-taxes-block'       => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryTaxesBlock::class,
+			'cart-order-summary-totals-block'      => \Automattic\WooCommerce\Blocks\BlockTypes\CartOrderSummaryTotalsBlock::class,
+			'cart-totals-block'                    => \Automattic\WooCommerce\Blocks\BlockTypes\CartTotalsBlock::class,
+			'empty-cart-block'                     => \Automattic\WooCommerce\Blocks\BlockTypes\EmptyCartBlock::class,
+			'filled-cart-block'                    => \Automattic\WooCommerce\Blocks\BlockTypes\FilledCartBlock::class,
+		);
+	}
+
+	/**
+	 * Side effect free and stateless Checkout inner blocks.
+	 *
+	 * @return array<string,string>
+	 */
+	private function get_checkout_inner_blocks(): array {
+		// CheckoutExpressPaymentBlock: excluded — overrides initialize() with save_post hook for express payment attribute sync.
+		return array(
+			'checkout-actions-block'                   => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutActionsBlock::class,
+			'checkout-additional-information-block'    => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutAdditionalInformationBlock::class,
+			'checkout-billing-address-block'           => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutBillingAddressBlock::class,
+			'checkout-contact-information-block'       => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutContactInformationBlock::class,
+			'checkout-fields-block'                    => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutFieldsBlock::class,
+			'checkout-order-note-block'                => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderNoteBlock::class,
+			'checkout-order-summary-block'             => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryBlock::class,
+			'checkout-order-summary-cart-items-block'  => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryCartItemsBlock::class,
+			'checkout-order-summary-coupon-form-block' => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryCouponFormBlock::class,
+			'checkout-order-summary-discount-block'    => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryDiscountBlock::class,
+			'checkout-order-summary-fee-block'         => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryFeeBlock::class,
+			'checkout-order-summary-shipping-block'    => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryShippingBlock::class,
+			'checkout-order-summary-subtotal-block'    => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummarySubtotalBlock::class,
+			'checkout-order-summary-taxes-block'       => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryTaxesBlock::class,
+			'checkout-order-summary-totals-block'      => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutOrderSummaryTotalsBlock::class,
+			'checkout-payment-block'                   => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutPaymentBlock::class,
+			'checkout-pickup-options-block'            => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutPickupOptionsBlock::class,
+			'checkout-shipping-address-block'          => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutShippingAddressBlock::class,
+			'checkout-shipping-method-block'           => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutShippingMethodBlock::class,
+			'checkout-shipping-methods-block'          => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutShippingMethodsBlock::class,
+			'checkout-terms-block'                     => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutTermsBlock::class,
+			'checkout-totals-block'                    => \Automattic\WooCommerce\Blocks\BlockTypes\CheckoutTotalsBlock::class,
+		);
+	}
+
+	/**
+	 * Side effect free and stateless MiniCart inner blocks.
+	 *
+	 * @return array<string,string>
+	 */
+	private function get_mini_cart_inner_blocks(): array {
+		// MiniCartFooterBlock: excluded — overrides enqueue_data() with editor-visible footer description.
+		return array(
+			'empty-mini-cart-contents-block'      => \Automattic\WooCommerce\Blocks\BlockTypes\EmptyMiniCartContentsBlock::class,
+			'filled-mini-cart-contents-block'     => \Automattic\WooCommerce\Blocks\BlockTypes\FilledMiniCartContentsBlock::class,
+			'mini-cart-cart-button-block'         => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartCartButtonBlock::class,
+			'mini-cart-checkout-button-block'     => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartCheckoutButtonBlock::class,
+			'mini-cart-items-block'               => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartItemsBlock::class,
+			'mini-cart-products-table-block'      => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartProductsTableBlock::class,
+			'mini-cart-shopping-button-block'     => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartShoppingButtonBlock::class,
+			'mini-cart-title-block'               => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartTitleBlock::class,
+			'mini-cart-title-items-counter-block' => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartTitleItemsCounterBlock::class,
+			'mini-cart-title-label-block'         => \Automattic\WooCommerce\Blocks\BlockTypes\MiniCartTitleLabelBlock::class,
+		);
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypesLazyControllerTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypesLazyControllerTest.php
new file mode 100644
index 00000000000..7e42f85c156
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypesLazyControllerTest.php
@@ -0,0 +1,83 @@
+<?php declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks;
+
+use Automattic\WooCommerce\Blocks\Assets\Api;
+use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
+use Automattic\WooCommerce\Blocks\BlockTypes\CartItemsBlock;
+use Automattic\WooCommerce\Blocks\BlockTypes\CheckoutActionsBlock;
+use Automattic\WooCommerce\Blocks\BlockTypes\EmptyMiniCartContentsBlock;
+use Automattic\WooCommerce\Blocks\BlockTypesLazyController;
+use Automattic\WooCommerce\Blocks\Package;
+use Automattic\WooCommerce\Tests\Blocks\Helpers\BlockTypeRegistryTrait;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the BlockTypesLazyController class.
+ */
+final class BlockTypesLazyControllerTest extends WC_Unit_Test_Case {
+	use BlockTypeRegistryTrait;
+
+	/**
+	 * The blocks map for test-cases to operate on.
+	 *
+	 * @var array<string,string>
+	 */
+	private const TEST_BLOCKS = array(
+		CartItemsBlock::class             => 'woocommerce/cart-items-block',
+		CheckoutActionsBlock::class       => 'woocommerce/checkout-actions-block',
+		EmptyMiniCartContentsBlock::class => 'woocommerce/empty-mini-cart-contents-block',
+	);
+
+	/**
+	 * Controller instance.
+	 *
+	 * @var BlockTypesLazyController
+	 */
+	private $sut;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$this->sut = new BlockTypesLazyController(
+			Package::container()->get( Api::class ),
+			Package::container()->get( AssetDataRegistry::class )
+		);
+
+		$registry = \WP_Block_Type_Registry::get_instance();
+		foreach ( self::TEST_BLOCKS as $name ) {
+			$this->block_types_before_test[ $name ] = $registry->get_registered( $name );
+			if ( $registry->is_registered( $name ) ) {
+				$registry->unregister( $name );
+			}
+		}
+	}
+
+	/**
+	 * @testdox Should register whitelisted blocks and detect WP registry removal.
+	 */
+	public function test_registers_whitelisted_blocks_and_excludes_unregistered_on_repeat(): void {
+		$whitelist  = array_keys( self::TEST_BLOCKS );
+		$registered = $this->sut->register_blocks( $whitelist );
+
+		sort( $registered );
+		$this->assertSame( $whitelist, $registered, 'All whitelisted block classes should be registered.' );
+
+		$wp_registry = \WP_Block_Type_Registry::get_instance();
+		foreach ( self::TEST_BLOCKS as $name ) {
+			$this->assertTrue( $wp_registry->is_registered( $name ), "$name should be in WP block registry." );
+		}
+
+		$first_class = array_key_first( self::TEST_BLOCKS );
+		$first_name  = self::TEST_BLOCKS[ $first_class ];
+
+		$wp_registry->unregister( $first_name );
+		$repeat = $this->sut->register_blocks( $whitelist );
+
+		sort( $repeat );
+		$this->assertSame( array_values( array_diff( $whitelist, array( $first_class ) ) ), $repeat, "$first_name should be excluded from repeat call after WP registry removal." );
+	}
+}