Commit bfddbced1cd for woocommerce
commit bfddbced1cd5cbddc6c3853a6508695daf277762
Author: Raluca Stan <ralucastn@gmail.com>
Date: Thu Jun 25 16:26:04 2026 +0200
Performance: lazily load block pattern content (#65922)
* perf: lazily load block pattern content via core filePath mechanism
BlockPatterns::register_block_patterns() previously ran ob_start();
include; ob_get_clean() for all ~41 pattern files on every request
(front-end, REST, cron), costing ~7-8 ms even where patterns are never
consumed.
Register patterns with core's `filePath` property (WP 6.5+) instead, so
WP_Block_Patterns_Registry loads and caches the content lazily only when
a pattern is actually requested. PatternRegistry now accepts a pattern
that carries `filePath` in place of inline `content`. No content
processing was lost: the WC registry only translates metadata and
registers categories, none of which depend on the content.
Removes the now-obsolete load_pattern_content() PHPStan baseline entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf: guard lazy pattern filePath against missing files
Confirm a pattern's source file still exists before registering it with a
`filePath`. The pattern list can come from a stale site transient, and
without inline content core would emit an undefined-content warning when it
tried to lazily load a missing file on demand. This mirrors core's own
`_register_theme_block_patterns()` file_exists guard.
Adds a regression test for the skip path and points the cached-pattern test
at an existing fixture so the guard is satisfied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Guard pattern source against malformed cache entries
Validate that a cached pattern's `source` is a non-empty string before
dereferencing it to build the path, so a stale or malformed site transient
can't raise a PHP warning before the file_exists guard. Adds a regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/perf-lazy-block-patterns b/plugins/woocommerce/changelog/perf-lazy-block-patterns
new file mode 100644
index 00000000000..129c32f09d8
--- /dev/null
+++ b/plugins/woocommerce/changelog/perf-lazy-block-patterns
@@ -0,0 +1,3 @@
+Significance: patch
+Type: performance
+Comment: Load WooCommerce block pattern content lazily via core's `filePath` mechanism instead of including all pattern files on every request, saving ~7-8 ms on requests that do not consume patterns.
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index c6f0a356591..60057b96c5b 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -49725,12 +49725,6 @@ parameters:
count: 1
path: src/Blocks/AssetsController.php
- -
- message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockPatterns\:\:load_pattern_content\(\) should return string but returns string\|false\.$#'
- identifier: return.type
- count: 1
- path: src/Blocks/BlockPatterns.php
-
-
message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockPatterns\:\:set_pattern_cache\(\) has no return type specified\.$#'
identifier: missingType.return
diff --git a/plugins/woocommerce/src/Blocks/BlockPatterns.php b/plugins/woocommerce/src/Blocks/BlockPatterns.php
index 3767a854c5d..1a3c939fc44 100644
--- a/plugins/woocommerce/src/Blocks/BlockPatterns.php
+++ b/plugins/woocommerce/src/Blocks/BlockPatterns.php
@@ -80,25 +80,16 @@ class BlockPatterns {
}
}
- /**
- * Loads the content of a pattern.
- *
- * @param string $pattern_path The path to the pattern.
- * @return string The content of the pattern.
- */
- private function load_pattern_content( $pattern_path ) {
- if ( ! file_exists( $pattern_path ) ) {
- return '';
- }
-
- ob_start();
- include $pattern_path;
- return ob_get_clean();
- }
-
/**
* Register block patterns from core.
*
+ * The pattern content is not loaded here. Instead, each pattern is registered
+ * with the absolute path to its source file (via the `filePath` property), so
+ * that core's `WP_Block_Patterns_Registry` loads (and caches) the content
+ * lazily, only when a pattern is actually requested. This avoids the cost of
+ * `include`-ing all pattern files on every request (e.g. front-end, REST,
+ * cron), where patterns are never consumed.
+ *
* @return void
*/
public function register_block_patterns() {
@@ -108,11 +99,25 @@ class BlockPatterns {
$patterns = $this->get_block_patterns();
foreach ( $patterns as $pattern ) {
- $pattern_path = $this->patterns_path . '/' . $pattern['source'];
- $pattern['source'] = $pattern_path;
+ // The pattern list can come from a stale or malformed site transient, so make sure the source is a
+ // usable string before dereferencing it, to avoid a PHP warning when building the path.
+ if ( empty( $pattern['source'] ) || ! is_string( $pattern['source'] ) ) {
+ continue;
+ }
+
+ $pattern_path = $this->patterns_path . '/' . $pattern['source'];
+
+ // The pattern list can come from a stale cache, so confirm the file
+ // still exists before registering it with a `filePath`. Without
+ // inline content, core would otherwise emit an undefined-content
+ // warning when it tries to load a missing file on demand. This
+ // mirrors core's own `_register_theme_block_patterns()` guard.
+ if ( ! file_exists( $pattern_path ) ) {
+ continue;
+ }
- $content = $this->load_pattern_content( $pattern_path );
- $pattern['content'] = $content;
+ $pattern['source'] = $pattern_path;
+ $pattern['filePath'] = $pattern_path;
$this->pattern_registry->register_block_pattern( $pattern_path, $pattern );
}
diff --git a/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php b/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php
index dc7177ac5de..04a5453614c 100644
--- a/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php
+++ b/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php
@@ -149,10 +149,20 @@ class PatternRegistry {
$pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', 'woocommerce' );
}
- if ( empty( $pattern_data['content'] ) ) {
+ // A pattern is registrable as long as it provides either inline content
+ // or a `filePath` that core can load lazily (WP 6.5+). Bail only when
+ // neither is available.
+ if ( empty( $pattern_data['content'] ) && empty( $pattern_data['filePath'] ) ) {
return;
}
+ // When a `filePath` is provided, let core load the content lazily on
+ // demand. Drop any empty `content` so core falls back to `filePath`
+ // instead of registering an empty pattern.
+ if ( ! empty( $pattern_data['filePath'] ) && empty( $pattern_data['content'] ) ) {
+ unset( $pattern_data['content'] );
+ }
+
$category_labels = $this->get_category_labels();
if ( ! empty( $pattern_data['categories'] ) ) {
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockPatterns/BlockPatterns.php b/plugins/woocommerce/tests/php/src/Blocks/BlockPatterns/BlockPatterns.php
index 552a026f587..e2b77199d4e 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockPatterns/BlockPatterns.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockPatterns/BlockPatterns.php
@@ -59,14 +59,8 @@ class BlockPatterns extends \WP_UnitTestCase {
*/
public function test_block_patterns_registration() {
- ob_start();
- include __DIR__ . '/patterns/mock-header.php';
- $mock_header_content = ob_get_clean();
-
- ob_start();
- include __DIR__ . '/patterns/mock-footer.php';
- $mock_footer_content = ob_get_clean();
-
+ // Content is no longer loaded eagerly. Patterns are registered with a
+ // `filePath` so core can load the content lazily on demand.
$this->pattern_registry
->expects( $this->exactly( 2 ) )
->method( 'register_block_pattern' )
@@ -85,7 +79,7 @@ class BlockPatterns extends \WP_UnitTestCase {
'featureFlag' => '',
'templateTypes' => '',
'source' => __DIR__ . '/patterns/mock-footer.php',
- 'content' => $mock_footer_content,
+ 'filePath' => __DIR__ . '/patterns/mock-footer.php',
),
),
array(
@@ -102,7 +96,7 @@ class BlockPatterns extends \WP_UnitTestCase {
'featureFlag' => '',
'templateTypes' => '',
'source' => __DIR__ . '/patterns/mock-header.php',
- 'content' => $mock_header_content,
+ 'filePath' => __DIR__ . '/patterns/mock-header.php',
),
),
);
@@ -114,10 +108,13 @@ class BlockPatterns extends \WP_UnitTestCase {
* Tests if patterns are registered with the cached data.
*/
public function test_cached_block_patterns_registration() {
+ // The cached source points at a file that still exists on disk, so it is
+ // registered with a `filePath` for lazy loading. The registry is mocked,
+ // so the file is never actually read; only its existence matters here.
$mock_patterns = array(
array(
'title' => 'Mock Cached',
- 'source' => 'mock-cached.php',
+ 'source' => 'mock-header.php',
'content' => '',
),
);
@@ -128,21 +125,76 @@ class BlockPatterns extends \WP_UnitTestCase {
set_site_transient( 'woocommerce_blocks_patterns', $pattern_data );
- $expected_pattern = $mock_patterns[0];
- $expected_pattern['source'] = __DIR__ . '/patterns/mock-cached.php';
- $expected_pattern['content'] = '';
+ $expected_pattern = $mock_patterns[0];
+ $expected_pattern['source'] = __DIR__ . '/patterns/mock-header.php';
+ $expected_pattern['content'] = '';
+ $expected_pattern['filePath'] = __DIR__ . '/patterns/mock-header.php';
$this->pattern_registry
->expects( $this->exactly( 1 ) )
->method( 'register_block_pattern' )
->with(
- __DIR__ . '/patterns/mock-cached.php',
+ __DIR__ . '/patterns/mock-header.php',
$expected_pattern,
);
$this->block_patterns->register_block_patterns();
}
+ /**
+ * Tests that a cached pattern whose source file no longer exists on disk is
+ * skipped, so core is never asked to lazily load a missing file.
+ */
+ public function test_cached_pattern_with_missing_file_is_skipped() {
+ $pattern_data = array(
+ 'version' => WOOCOMMERCE_VERSION,
+ 'patterns' => array(
+ array(
+ 'title' => 'Mock Missing',
+ 'source' => 'does-not-exist.php',
+ 'content' => '',
+ ),
+ ),
+ );
+
+ set_site_transient( 'woocommerce_blocks_patterns', $pattern_data );
+
+ $this->pattern_registry
+ ->expects( $this->never() )
+ ->method( 'register_block_pattern' );
+
+ $this->block_patterns->register_block_patterns();
+ }
+
+ /**
+ * Tests that a cached pattern with a missing or invalid source is skipped, so the source is never
+ * dereferenced (which would raise a PHP warning) when building the pattern path.
+ */
+ public function test_cached_pattern_with_invalid_source_is_skipped() {
+ $pattern_data = array(
+ 'version' => WOOCOMMERCE_VERSION,
+ 'patterns' => array(
+ array(
+ 'title' => 'No Source',
+ 'content' => '',
+ ),
+ array(
+ 'title' => 'Invalid Source',
+ 'source' => array( 'not', 'a', 'string' ),
+ 'content' => '',
+ ),
+ ),
+ );
+
+ set_site_transient( 'woocommerce_blocks_patterns', $pattern_data );
+
+ $this->pattern_registry
+ ->expects( $this->never() )
+ ->method( 'register_block_pattern' );
+
+ $this->block_patterns->register_block_patterns();
+ }
+
/**
* Tests if patterns are registered with the cached data.
*/
@@ -160,14 +212,8 @@ class BlockPatterns extends \WP_UnitTestCase {
set_site_transient( 'woocommerce_blocks_patterns', $pattern_data );
- ob_start();
- include __DIR__ . '/patterns/mock-header.php';
- $mock_header_content = ob_get_clean();
-
- ob_start();
- include __DIR__ . '/patterns/mock-footer.php';
- $mock_footer_content = ob_get_clean();
-
+ // Content is no longer loaded eagerly. Patterns are registered with a
+ // `filePath` so core can load the content lazily on demand.
$this->pattern_registry
->expects( $this->exactly( 2 ) )
->method( 'register_block_pattern' )
@@ -186,7 +232,7 @@ class BlockPatterns extends \WP_UnitTestCase {
'featureFlag' => '',
'templateTypes' => '',
'source' => __DIR__ . '/patterns/mock-footer.php',
- 'content' => $mock_footer_content,
+ 'filePath' => __DIR__ . '/patterns/mock-footer.php',
),
),
array(
@@ -203,7 +249,7 @@ class BlockPatterns extends \WP_UnitTestCase {
'featureFlag' => '',
'templateTypes' => '',
'source' => __DIR__ . '/patterns/mock-header.php',
- 'content' => $mock_header_content,
+ 'filePath' => __DIR__ . '/patterns/mock-header.php',
),
),
);