Commit db9100d66b6 for woocommerce
commit db9100d66b6081c6eee0c9ebe52d89e2ca379601
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date: Tue Oct 20 07:55:02 2026 +0200
[Performance] Reduce Woo template parts resolution footprint during page generation (#69629)
Technically, it's a micro-optimization, but returning control to WordPress core fixes false attribution to Woo in the flame graph (e.g., for header and footer generation). This is necessary for performance work where flame graphs are a primary driver in finding bottlenecks.
diff --git a/plugins/woocommerce/changelog/performance-69369-template-parts-resolution b/plugins/woocommerce/changelog/performance-69369-template-parts-resolution
new file mode 100644
index 00000000000..583d9bd8359
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-69369-template-parts-resolution
@@ -0,0 +1,4 @@
+Significance: patch
+Type: performance
+
+Reduce Woo template parts resolution footprint during page generation.
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 659fd3ce65a..ec2b2b75497 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -48357,12 +48357,6 @@ parameters:
count: 1
path: src/Blocks/BlockTemplatesController.php
- -
- message: '#^Callback expects 1 parameter, \$accepted_args is set to 3\.$#'
- identifier: arguments.count
- count: 1
- path: src/Blocks/BlockTemplatesController.php
-
-
message: '#^Default value of the parameter \#2 \$template_type \(string\) of method Automattic\\WooCommerce\\Blocks\\BlockTemplatesController\:\:block_template_is_available\(\) is incompatible with type array\.$#'
identifier: parameter.defaultValue
@@ -48387,12 +48381,6 @@ parameters:
count: 1
path: src/Blocks/BlockTemplatesController.php
- -
- message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTemplatesController\:\:add_plugin_templates_parts_support\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/Blocks/BlockTemplatesController.php
-
-
message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTemplatesController\:\:dequeue_legacy_scripts\(\) has no return type specified\.$#'
identifier: missingType.return
diff --git a/plugins/woocommerce/src/Blocks/BlockTemplatesController.php b/plugins/woocommerce/src/Blocks/BlockTemplatesController.php
index 3e1a99a32c7..b5eb0370401 100644
--- a/plugins/woocommerce/src/Blocks/BlockTemplatesController.php
+++ b/plugins/woocommerce/src/Blocks/BlockTemplatesController.php
@@ -25,20 +25,13 @@ class BlockTemplatesController {
*/
public function init() {
add_filter( 'pre_get_block_file_template', array( $this, 'get_block_file_template' ), 10, 3 );
+ add_filter( 'pre_render_block', array( $this, 'pre_render_woocommerce_template_part' ), 10, 3 );
add_filter( 'get_block_template', array( $this, 'add_block_template_details' ), 10, 3 );
add_filter( 'get_block_templates', array( $this, 'add_db_templates_with_woo_slug' ), 10, 3 );
add_filter( 'rest_pre_insert_wp_template', array( $this, 'dont_load_templates_for_suggestions' ), 10, 1 );
- add_filter( 'block_type_metadata_settings', array( $this, 'add_plugin_templates_parts_support' ), 10, 2 );
add_filter( 'block_type_metadata_settings', array( $this, 'prevent_shortcodes_html_breakage' ), 10, 2 );
add_action( 'current_screen', array( $this, 'hide_template_selector_in_cart_checkout_pages' ), 10 );
add_action( 'wp_enqueue_scripts', [ $this, 'dequeue_legacy_scripts' ], 20 );
-
- // Fix a bug in WordPress 6.8 and lower that caused block hooks not to
- // run in templates registered via the Template Registration API.
- // @see https://github.com/WordPress/gutenberg/issues/71139.
- if ( Utils::wp_version_compare( '6.8', '<=' ) ) {
- add_filter( 'get_block_templates', array( $this, 'run_hooks_on_block_templates' ), 10, 3 );
- }
}
/**
@@ -54,16 +47,70 @@ class BlockTemplatesController {
}
}
+ /**
+ * Short-circuits rendering of `core/template-part` blocks that belong to WooCommerce (header, footer, navigation etc. are skipped).
+ *
+ * @since 11.4.0
+ * @internal
+ *
+ * @param string|null $pre_render The pre-rendered content, or null to let the block render normally.
+ * @param array $parsed_block The block being rendered.
+ * @param \WP_Block|null $parent_block The parent block, if any.
+ * @return string|null
+ */
+ public function pre_render_woocommerce_template_part( $pre_render, $parsed_block, $parent_block = null ) {
+ // Upstream render_block_core_template_part can't resolve plugin-shipped template parts. This intercepts only woocommerce/woocommerce
+ // parts via pre_render_block; non-WooCommerce parts (header, footer, etc.) reach core directly with zero overhead.
+ if ( null === $pre_render && 'core/template-part' === ( $parsed_block['blockName'] ?? null ) && 'woocommerce/woocommerce' === ( $parsed_block['attrs']['theme'] ?? null ) ) {
+ global $post;
+
+ // BC (block_type_metadata_settings → pre_render_block migration): re-apply render_block() input filters.
+ // Skipped: enqueue_empty_block_content_assets (template parts always have content).
+ $parsed_block = apply_filters( 'render_block_data', $parsed_block, $parsed_block, $parent_block ); // phpcs:ignore WooCommerce.Commenting.CommentHooks
+ $context = $post instanceof \WP_Post ? array( 'postId' => $post->ID, 'postType' => $post->post_type ) : array(); // phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing
+ $context = apply_filters( 'render_block_context', $context, $parsed_block, $parent_block ); // phpcs:ignore WooCommerce.Commenting.CommentHooks
+
+ if ( is_array( $context ) && is_array( $parsed_block ) && isset( $parsed_block['attrs']['theme'], $parsed_block['attrs']['slug'] ) ) {
+ $template_part = get_block_template( $parsed_block['attrs']['theme'] . '//' . $parsed_block['attrs']['slug'], 'wp_template_part' );
+ if ( $template_part && ! empty( $template_part->content ) ) {
+ $content = do_blocks( $template_part->content );
+
+ // BC: Set block-support context so get_block_wrapper_attributes resolves this block's attributes, not a parent's.
+ list( $previous_block, \WP_Block_Supports::$block_to_render ) = array( \WP_Block_Supports::$block_to_render, $parsed_block );
+ list( $wrapper_attributes, \WP_Block_Supports::$block_to_render ) = array( get_block_wrapper_attributes(), $previous_block );
+
+ $html_tag = $parsed_block['attrs']['tagName'] ?? null;
+ $html_tag = ( $html_tag && tag_escape( $html_tag ) === $html_tag ) ? esc_attr( $html_tag ) : 'div';
+ $block_content = "<$html_tag $wrapper_attributes>" . str_replace( ']]>', ']]>', $content ) . "</$html_tag>";
+
+ // BC (block_type_metadata_settings → pre_render_block migration): re-apply WP_Block::render() output filters.
+ // Skipped: interactivity_process_directives (template parts aren't interactive roots).
+ $block_instance = new \WP_Block( $parsed_block, $context );
+ $block_content = apply_filters( 'render_block', $block_content, $parsed_block, $block_instance ); // phpcs:ignore WooCommerce.Commenting.CommentHooks
+ $block_content = apply_filters( 'render_block_core/template-part', $block_content, $parsed_block, $block_instance ); // phpcs:ignore WordPress.NamingConventions.ValidHookName, WooCommerce.Commenting.CommentHooks
+
+ return $block_content;
+ }
+ }
+ }
+
+ return $pre_render;
+ }
+
/**
* Renders the `core/template-part` block on the server.
+ * Introduced because the core handling for template parts only supports templates from the current theme, not from a plugin.
*
- * This is done because the core handling for template parts only supports templates from the current theme, not
- * from a plugin.
+ * @deprecated 11.4.0
*
* @param array $attributes The block attributes.
- * @return string The render.
+ * @return string
*/
public function render_woocommerce_template_part( $attributes ) {
+ // Superseded by pre_render_woocommerce_template_part. The old approach (add_plugin_templates_parts_support) replaced
+ // the render_callback for all core/template-part blocks, not just WooCommerce ones.
+ wc_deprecated_function( __METHOD__, '11.4.0' );
+
if ( isset( $attributes['theme'] ) && 'woocommerce/woocommerce' === $attributes['theme'] ) {
$template_part = get_block_template( $attributes['theme'] . '//' . $attributes['slug'], 'wp_template_part' );
@@ -87,10 +134,16 @@ class BlockTemplatesController {
* By default, the Template Part Block only supports template parts that are in the current theme directory.
* This render_callback wrapper allows us to add support for plugin-housed template parts.
*
+ * @deprecated 11.4.0
+ *
* @param array $settings Array of determined settings for registering a block type.
* @param array $metadata Metadata provided for registering a block type.
+ *
+ * @return array
*/
public function add_plugin_templates_parts_support( $settings, $metadata ) {
+ wc_deprecated_function( __METHOD__, '11.4.0' );
+
if (
isset( $metadata['name'], $settings['render_callback'] ) &&
'core/template-part' === $metadata['name'] &&
@@ -220,12 +273,16 @@ class BlockTemplatesController {
}
/**
- * Run hooks on block templates.
+ * Run hooks on block templates. Previous was addressing https://github.com/WordPress/gutenberg/issues/71139 on WordPress 6.8 and lower.
+ *
+ * @deprecated 11.4.0
*
* @param array $templates The block templates.
* @return array The block templates.
*/
public function run_hooks_on_block_templates( $templates ) {
+ wc_deprecated_function( __METHOD__, '11.4.0' );
+
foreach ( $templates as $template ) {
if ( 'plugin' === $template->source && 'woocommerce' === $template->plugin ) {
$template->content = apply_block_hooks_to_content( $template->content, $template, 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata' );
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTemplatesControllerTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTemplatesControllerTest.php
index 74ea6932c2c..6fe643233dd 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTemplatesControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTemplatesControllerTest.php
@@ -169,6 +169,74 @@ class BlockTemplatesControllerTest extends WC_Unit_Test_Case {
return get_post( $post_id );
}
+ /**
+ * @testdox WooCommerce template part is resolvable through get_block_template.
+ */
+ public function test_woocommerce_template_part_renders_content(): void {
+ $this->create_template_part( 'test-woo-part', BlockTemplateUtils::PLUGIN_SLUG );
+ $this->flush_block_template_caches();
+
+ $template_part = get_block_template( BlockTemplateUtils::PLUGIN_SLUG . '//test-woo-part', 'wp_template_part' );
+ $this->assertSame( '<!-- wp:paragraph --><p>Test</p><!-- /wp:paragraph -->', $template_part->content ?? null, 'Template part should contain the expected content.' );
+ }
+
+ /**
+ * @testdox Missing WooCommerce template part resolves to null.
+ */
+ public function test_missing_woocommerce_template_part_resolves_to_null(): void {
+ $template_part = get_block_template( BlockTemplateUtils::PLUGIN_SLUG . '//non-existent-part-' . uniqid(), 'wp_template_part' );
+ $this->assertNull( $template_part, 'Missing template part should resolve to null.' );
+ }
+
+ /**
+ * @testdox BC filters (render_block_data, render_block, render_block_core/template-part) fire for WooCommerce template parts.
+ */
+ public function test_bc_filters_fire_for_woocommerce_template_part(): void {
+ $this->sut->init();
+
+ $this->create_template_part( 'test-filtered-part', BlockTemplateUtils::PLUGIN_SLUG );
+ $this->flush_block_template_caches();
+
+ $block_filter = static fn( $block ) => array_merge_recursive( $block, ( 'core/template-part' === ( $block['blockName'] ?? null ) ? array( 'attrs' => array( 'className' => 'injected-by-filter' ) ) : array() ) );
+ $output_filter = static fn( $content, $block ) => $content . ( 'core/template-part' === ( $block['blockName'] ?? null ) ? '<!-- filtered -->' : '' );
+ add_filter( 'render_block_data', $block_filter, 10, 1 );
+ add_filter( 'render_block', $output_filter, 10, 2 );
+ add_filter( 'render_block_core/template-part', $output_filter, 10, 2 );
+
+ $parsed_block = array(
+ 'blockName' => 'core/template-part',
+ 'attrs' => array(
+ 'theme' => 'woocommerce/woocommerce',
+ 'slug' => 'test-filtered-part',
+ 'tagName' => 'span',
+ ),
+ );
+ $result = $this->sut->pre_render_woocommerce_template_part( null, $parsed_block );
+
+ remove_filter( 'render_block_data', $block_filter, 10 );
+ remove_filter( 'render_block', $output_filter, 10 );
+ remove_filter( 'render_block_core/template-part', $output_filter, 10 );
+
+ $expected = '<span class="injected-by-filter wp-block-template-part"><p class="wp-block-paragraph">Test</p></span><!-- filtered --><!-- filtered -->';
+ $this->assertSame( $expected, $result, 'render_block_data, render_block, and render_block_core/template-part filter output should all be included.' );
+ }
+
+ /**
+ * @testdox WooCommerce template part with className renders it in the wrapper.
+ */
+ public function test_woocommerce_template_part_renders_classname_in_wrapper(): void {
+ $this->sut->init();
+
+ $this->create_template_part( 'test-classed-part', BlockTemplateUtils::PLUGIN_SLUG );
+ $this->flush_block_template_caches();
+
+ $markup = '<!-- wp:template-part {"slug":"test-classed-part","theme":"woocommerce/woocommerce","className":"my-custom-class"} /-->';
+ $output = do_blocks( $markup );
+
+ $expected = '<div class="my-custom-class wp-block-template-part"><p class="wp-block-paragraph">Test</p></div>';
+ $this->assertSame( $expected, $output, 'Custom className should appear in the rendered wrapper.' );
+ }
+
/**
* Clears template ID caches so newly created posts are visible.
*/