Commit 5f95f9774cf for woocommerce

commit 5f95f9774cf2072549d9b9570012dc160503eea8
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date:   Tue Oct 20 07:53:26 2026 +0200

    [Performance] Lazy-load block template files content in the registry (#69749)

    This PR optimizes \Automattic\WooCommerce\Blocks\BlockTemplatesRegistry::init to skip eager loading (8 files are read per request, which are not backed by OPcache). Instead, we lazily load the contents as needed.

diff --git a/plugins/woocommerce/changelog/performance-69369-blocks-lazy-get-block-file-template b/plugins/woocommerce/changelog/performance-69369-blocks-lazy-get-block-file-template
new file mode 100644
index 00000000000..c81faefbc97
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-69369-blocks-lazy-get-block-file-template
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Lazy-load block template files content in the registry.
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 978bbbd236d..659fd3ce65a 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -48471,12 +48471,6 @@ parameters:
 			count: 1
 			path: src/Blocks/BlockTemplatesRegistry.php

-		-
-			message: '#^Parameter \#2 \$args of function register_block_template expects array\{title\?\: string, description\?\: string, content\?\: string, post_types\?\: array\<string\>, plugin\?\: string\}, array\{title\: string, description\: string, content\: string\|false\} given\.$#'
-			identifier: argument.type
-			count: 1
-			path: src/Blocks/BlockTemplatesRegistry.php
-
 		-
 			message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractBlock\:\:enqueue_assets\(\) has no return type specified\.$#'
 			identifier: missingType.return
diff --git a/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php b/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php
index ac5717e4c08..b2aa4e5a6ce 100644
--- a/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php
+++ b/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php
@@ -38,10 +38,18 @@ class BlockTemplatesRegistry {
 	 */
 	private $templates = array();

+	/**
+	 * Authoritative map for registered template name ('woocommerce//slug') to its HTML file path.
+	 *
+	 * @var array<string,string>
+	 */
+	private $template_paths = array();
+
 	/**
 	 * Initialization method.
 	 */
 	public function init() {
+		$templates = array();
 		if ( BlockTemplateUtils::supports_block_templates( 'wp_template' ) ) {
 			$templates = array(
 				ProductCatalogTemplate::SLUG       => new ProductCatalogTemplate(),
@@ -55,10 +63,10 @@ class BlockTemplatesRegistry {
 				OrderConfirmationTemplate::SLUG    => new OrderConfirmationTemplate(),
 				SingleProductTemplate::SLUG        => new SingleProductTemplate(),
 			);
-		} else {
-			$templates = array();
 		}
 		$templates[ ComingSoonTemplate::SLUG ] = new ComingSoonTemplate();
+
+		$template_parts = array();
 		if ( BlockTemplateUtils::supports_block_templates( 'wp_template_part' ) ) {
 			$template_parts = array(
 				MiniCartTemplate::SLUG       => new MiniCartTemplate(),
@@ -82,28 +90,27 @@ class BlockTemplatesRegistry {
 					}
 				}
 			}
-		} else {
-			$template_parts = array();
 		}

-		// Init all templates.
 		$directory = BlockTemplateUtils::get_templates_directory( 'wp_template' );
 		foreach ( $templates as $template ) {
 			$template->init();

-			// Taxonomy templates are registered automatically by WordPress and are made available through the Add Template menu.
+			// Taxonomy templates are registered automatically by WordPress and are added through the Add Template menu.
 			if ( ! $template->is_taxonomy_template ) {
+				$template_name                          = 'woocommerce//' . $template::SLUG;
+				$this->template_paths[ $template_name ] = $directory . '/' . $template::SLUG . '.html';
 				register_block_template(
-					'woocommerce//' . $template::SLUG,
+					$template_name,
 					array(
 						'title'       => $template->get_template_title(),
 						'description' => $template->get_template_description(),
-						// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
-						'content'     => file_get_contents( $directory . '/' . $template::SLUG . '.html' ),
 					)
 				);
 			}
 		}
+		add_filter( 'pre_get_block_templates', array( $this, 'lazy_load_requested_templates_content' ), 0, 3 );
+		add_filter( 'pre_get_block_file_template', array( $this, 'lazy_load_single_template_content' ), 0, 3 );

 		foreach ( $template_parts as $template_part ) {
 			$template_part->init();
@@ -129,6 +136,52 @@ class BlockTemplatesRegistry {
 		return array_merge( $default_area_definitions, array( $add_to_cart_with_options_template_part_area ) );
 	}

+	/**
+	 * Lazily load registered WooCommerce template file contents into the registry.
+	 *
+	 * @since 11.4.0
+	 * @internal
+	 *
+	 * @param mixed  $result        Pass-through (null to let WP continue).
+	 * @param array  $query         Query arguments.
+	 * @param string $template_type Template type.
+	 * @return mixed Unmodified $result.
+	 */
+	public function lazy_load_requested_templates_content( $result, $query, $template_type ) {
+		if ( 'wp_template' === $template_type && ! empty( $this->template_paths ) && ! isset( $query['wp_id'] ) ) {
+			// On front-end pages slug__in defines the target slugs; in other contexts we need to load all templates.
+			$target_templates = array_map( static fn( $slug ) => 'woocommerce//' . $slug, $query['slug__in'] ?? array() );
+			$target_templates = empty( $target_templates ) ? $this->template_paths : array_intersect_key( $this->template_paths, array_flip( $target_templates ) );
+			if ( ! empty( $target_templates ) ) {
+				$registry = \WP_Block_Templates_Registry::get_instance();
+				foreach ( $target_templates as $template_name => $template_path ) {
+					$template = $registry->get_registered( $template_name );
+					if ( $template && '' === $template->content ) {
+						$template->content = (string) file_get_contents( $template_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions
+					}
+					unset( $this->template_paths[ $template_name ] );
+				}
+			}
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Covers the singular resolution path (get_block_file_template) which bypasses get_block_templates() entirely.
+	 *
+	 * @since 11.4.0
+	 * @internal
+	 *
+	 * @param mixed  $result        Pass-through (null to let WP continue).
+	 * @param string $id            Template identifier (e.g. 'theme_name//slug').
+	 * @param string $template_type Template type.
+	 * @return mixed Unmodified $result.
+	 */
+	public function lazy_load_single_template_content( $result, $id, $template_type ) {
+		return $this->lazy_load_requested_templates_content( $result, array( 'slug__in' => array( explode( '//', $id, 2 )[1] ?? '' ) ), $template_type );
+	}
+
 	/**
 	 * Returns the template matching the slug
 	 *
@@ -137,10 +190,6 @@ class BlockTemplatesRegistry {
 	 * @return AbstractTemplate|AbstractTemplatePart|null
 	 */
 	public function get_template( $template_slug ) {
-		if ( array_key_exists( $template_slug, $this->templates ) ) {
-			$registered_template = $this->templates[ $template_slug ];
-			return $registered_template;
-		}
-		return null;
+		return $this->templates[ $template_slug ] ?? null;
 	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTemplatesRegistryTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTemplatesRegistryTest.php
new file mode 100644
index 00000000000..031d0af6272
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTemplatesRegistryTest.php
@@ -0,0 +1,36 @@
+<?php declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks;
+
+use Automattic\WooCommerce\Blocks\BlockTemplatesRegistry;
+use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the BlockTemplatesRegistry lazy-load content population.
+ */
+final class BlockTemplatesRegistryTest extends WC_Unit_Test_Case {
+	/**
+	 * @testdox lazy_load_requested_templates_content populates a template matching slug__in.
+	 */
+	public function test_lazy_load_populates_template_by_slug(): void {
+		$sut = wc_get_container()->get( BlockTemplatesRegistry::class );
+		// On classic themes Bootstrap skips init(), so register the hook directly.
+		add_filter( 'pre_get_block_templates', array( $sut, 'lazy_load_requested_templates_content' ), 0, 3 );
+
+		// Inject the checkout template path (init() on classic theme only registers ComingSoonTemplate).
+		$directory  = BlockTemplateUtils::get_templates_directory( 'wp_template' );
+		$reflection = new \ReflectionProperty( BlockTemplatesRegistry::class, 'template_paths' );
+		$reflection->setAccessible( true );
+		$reflection->setValue( $sut, array_merge( $reflection->getValue( $sut ), array( 'woocommerce//page-checkout' => $directory . '/page-checkout.html' ) ) );
+
+		register_block_template( 'woocommerce//page-checkout', array( 'title' => 'Checkout' ) );
+		$template = \WP_Block_Templates_Registry::get_instance()->get_registered( 'woocommerce//page-checkout' );
+
+		$this->assertSame( '', $template->content, 'Template content should start empty before lazy-load.' );
+
+		get_block_templates( array( 'slug__in' => array( 'page-checkout' ) ), 'wp_template' );
+
+		$this->assertStringContainsString( '<main class="wp-block-group">', $template->content, 'Content should contain WooCommerce markup.' );
+	}
+}