Skip to content

Commit fedd5a9

Browse files
committed
Interactivity API: Implement directive caching to optimize data-wp-each processing.
Previous attempts to optimize `WP_Interactivity_API::data_wp_each_processor` included a "pre-compute and cache" strategy. Instead of re-parsing the template on every iteration, we can build a "blueprint" of the template once, cache it, and then use it to efficiently process each item. However, this solution did not consider how the WP_HTML_Tag_Processor is designed. The WP_HTML_Tag_Processor architecture works by: - Sequentially scanning HTML to find tags (via strpos, strcspn, etc.) - Tracking positions as byte offsets - Modifying HTML through WP_HTML_Text_Replacement objects There's no way to "jump to position X" without scanning from the beginning. Byte offsets shift as HTML is modified, and bookmarks are designed for single-pass processing, not cross-rendering reuse. Looking more carefully at the bottleneck, the real issue is that for EACH item in the array, we: 1. Create a new WP_Interactivity_API_Directives_Processor 2. Call $p->next_tag() repeatedly (which does expensive string scanning) 3. Call get_attribute_names_with_prefix('data-wp-') for each tag 4. Parse directive names and extract values 5. Evaluate directives The optimization opportunity is steps 2-4, not step 5. We can't avoid re-scanning the HTML (that's how the Tag Processor works), but we can cache what we learned about where directives are and what they are. This commit introduces the `WP_Interactivity_API_Directive_Cache` class to cache pre-parsed directive information, significantly reducing the O(N×M) complexity of rendering templates with multiple items. The caching mechanism stores directive metadata, improving performance during repeated template rendering. Additionally, the `_process_directives` method is updated to utilize this cache, enhancing efficiency. Tests are added to ensure the correctness of the caching mechanism and its integration with existing functionality.
1 parent 6d7582a commit fedd5a9

File tree

7 files changed

+828
-27
lines changed

7 files changed

+828
-27
lines changed

src/wp-includes/assets/script-loader-packages.min.php

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<?php return array('interactivity/index.min.js' => array('dependencies' => array(), 'version' => '441cab46d043b0a45f6f', 'type' => 'module'), 'interactivity/debug.min.js' => array('dependencies' => array(), 'version' => '4b216ecdeb745ab1b420', 'type' => 'module'), 'interactivity-router/index.min.js' => array('dependencies' => array('@wordpress/interactivity', array('id' => '@wordpress/a11y', 'import' => 'dynamic')), 'version' => '765a6ee8162122b48e6c', 'type' => 'module'), 'interactivity-router/full-page.min.js' => array('dependencies' => array(array('id' => '@wordpress/interactivity-router', 'import' => 'dynamic')), 'version' => 'ac76172d5956969e2227', 'type' => 'module'), 'a11y/index.min.js' => array('dependencies' => array(), 'version' => 'b7d06936b8bc23cff2ad', 'type' => 'module'), 'block-library/accordion/view.min.js' => array('dependencies' => array('@wordpress/interactivity'), 'version' => '5fa6ee20ae87460b9868', 'type' => 'module'), 'block-library/file/view.min.js' => array('dependencies' => array('@wordpress/interactivity'), 'version' => 'f9665632b48682075277', 'type' => 'module'), 'block-library/form/view.min.js' => array('dependencies' => array(), 'version' => 'baaf25398238b4f2a821', 'type' => 'module'), 'block-library/image/view.min.js' => array('dependencies' => array('@wordpress/interactivity'), 'version' => '26816800d42394b0a5f5', 'type' => 'module'), 'block-library/navigation/view.min.js' => array('dependencies' => array('@wordpress/interactivity'), 'version' => '3d4d582d5a6b3cf1185b', 'type' => 'module'), 'block-library/query/view.min.js' => array('dependencies' => array('@wordpress/interactivity', array('id' => '@wordpress/interactivity-router', 'import' => 'dynamic')), 'version' => 'f55e93a1ad4806e91785', 'type' => 'module'), 'block-library/search/view.min.js' => array('dependencies' => array('@wordpress/interactivity'), 'version' => '208bf143e4074549fa89', 'type' => 'module'), 'block-editor/utils/fit-text-frontend.min.js' => array('dependencies' => array(), 'version' => '6e035d66824ec76d9de1', 'type' => 'module'));
1+
<?php return array('interactivity/index.min.js' => array('dependencies' => array(), 'version' => '441cab46d043b0a45f6f', 'type' => 'module'), 'interactivity/debug.min.js' => array('dependencies' => array(), 'version' => '4b216ecdeb745ab1b420', 'type' => 'module'), 'interactivity-router/index.min.js' => array('dependencies' => array('@wordpress/interactivity', array('id' => '@wordpress/a11y', 'import' => 'dynamic')), 'version' => '765a6ee8162122b48e6c', 'type' => 'module'), 'a11y/index.min.js' => array('dependencies' => array(), 'version' => 'b7d06936b8bc23cff2ad', 'type' => 'module'), 'block-library/file/view.min.js' => array('dependencies' => array('@wordpress/interactivity'), 'version' => 'f9665632b48682075277', 'type' => 'module'), 'block-library/form/view.min.js' => array('dependencies' => array(), 'version' => 'baaf25398238b4f2a821', 'type' => 'module'), 'block-library/image/view.min.js' => array('dependencies' => array('@wordpress/interactivity'), 'version' => '26816800d42394b0a5f5', 'type' => 'module'), 'block-library/navigation/view.min.js' => array('dependencies' => array('@wordpress/interactivity'), 'version' => '3d4d582d5a6b3cf1185b', 'type' => 'module'), 'block-library/query/view.min.js' => array('dependencies' => array('@wordpress/interactivity', array('id' => '@wordpress/interactivity-router', 'import' => 'dynamic')), 'version' => 'f55e93a1ad4806e91785', 'type' => 'module'), 'block-library/search/view.min.js' => array('dependencies' => array('@wordpress/interactivity'), 'version' => '208bf143e4074549fa89', 'type' => 'module'));
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<?php
2+
/**
3+
* Interactivity API: WP_Interactivity_API_Directive_Cache class
4+
*
5+
* @package WordPress
6+
* @subpackage Interactivity API
7+
* @since 6.10.0
8+
*/
9+
10+
/**
11+
* Caches pre-parsed directive information for efficient template re-rendering.
12+
*
13+
* This class addresses a performance bottleneck in `data-wp-each` processing where
14+
* the same template is rendered N times (once per array item), causing O(N×M)
15+
* complexity. By caching the directive parsing results, we reduce redundant work.
16+
*
17+
* The cache maps tag occurrence indices to pre-parsed directive data. Since the
18+
* WP_HTML_Tag_Processor requires sequential scanning, we use tag index to identify
19+
* which tag's metadata to retrieve.
20+
*
21+
* What gets cached:
22+
* - Parsed directive names (prefix, suffix, unique_id)
23+
* - Extracted directive values (namespace, value)
24+
* - Directive entries for each prefix type
25+
*
26+
* What does NOT get cached:
27+
* - Directive evaluation results (these depend on context)
28+
* - Modified HTML (each iteration needs a fresh template)
29+
* - The HTML scanning itself (unavoidable with Tag Processor architecture)
30+
*
31+
* @since 6.10.0
32+
*
33+
* @access private
34+
*
35+
* @see WP_Interactivity_API::data_wp_each_processor()
36+
*/
37+
class WP_Interactivity_API_Directive_Cache {
38+
/**
39+
* Cached directive entries organized by tag index and directive prefix.
40+
*
41+
* Structure:
42+
* [
43+
* '{tag_index}:{directive_prefix}' => [
44+
* ['namespace' => 'plugin', 'value' => 'context.foo', 'suffix' => null, 'unique_id' => null],
45+
* ...
46+
* ],
47+
* ...
48+
* ]
49+
*
50+
* @since 6.10.0
51+
* @var array
52+
*/
53+
private $cache = array();
54+
55+
/**
56+
* Cached attribute names with 'data-wp-' prefix for each tag.
57+
*
58+
* Structure:
59+
* [
60+
* {tag_index} => ['data-wp-text', 'data-wp-class--active', ...],
61+
* ...
62+
* ]
63+
*
64+
* @since 6.10.0
65+
* @var array
66+
*/
67+
private $directive_attributes_cache = array();
68+
69+
/**
70+
* Gets directive entries for a specific tag and prefix.
71+
*
72+
* If cached, returns the cached value. Otherwise, calls the parser function,
73+
* caches the result, and returns it.
74+
*
75+
* @since 6.10.0
76+
*
77+
* @param int $tag_index The tag occurrence index (0-based, increments with each tag that has directives).
78+
* @param string $prefix The directive prefix (e.g., 'text', 'bind', 'class').
79+
* @param callable $parser Function to call if not cached. Signature: function($tag_index, $prefix): array
80+
* @return array The directive entries for this tag and prefix.
81+
*/
82+
public function get_or_parse_entries( int $tag_index, string $prefix, callable $parser ): array {
83+
$cache_key = "{$tag_index}:{$prefix}";
84+
85+
if ( ! isset( $this->cache[ $cache_key ] ) ) {
86+
$this->cache[ $cache_key ] = $parser( $tag_index, $prefix );
87+
}
88+
89+
return $this->cache[ $cache_key ];
90+
}
91+
92+
/**
93+
* Caches the list of directive attribute names for a tag.
94+
*
95+
* @since 6.10.0
96+
*
97+
* @param int $tag_index The tag occurrence index.
98+
* @param array $directive_attributes The array of directive attribute names.
99+
*/
100+
public function cache_directive_attributes( int $tag_index, array $directive_attributes ): void {
101+
$this->directive_attributes_cache[ $tag_index ] = $directive_attributes;
102+
}
103+
104+
/**
105+
* Gets cached directive attribute names for a tag.
106+
*
107+
* @since 6.10.0
108+
*
109+
* @param int $tag_index The tag occurrence index.
110+
* @return array|null The directive attribute names, or null if not cached.
111+
*/
112+
public function get_directive_attributes( int $tag_index ): ?array {
113+
return $this->directive_attributes_cache[ $tag_index ] ?? null;
114+
}
115+
116+
/**
117+
* Checks if directive entries are cached for a specific tag and prefix.
118+
*
119+
* @since 6.10.0
120+
*
121+
* @param int $tag_index The tag occurrence index.
122+
* @param string $prefix The directive prefix.
123+
* @return bool Whether the entries are cached.
124+
*/
125+
public function has_cached_entries( int $tag_index, string $prefix ): bool {
126+
$cache_key = "{$tag_index}:{$prefix}";
127+
return isset( $this->cache[ $cache_key ] );
128+
}
129+
130+
/**
131+
* Clears all cached data.
132+
*
133+
* @since 6.10.0
134+
*/
135+
public function clear(): void {
136+
$this->cache = array();
137+
$this->directive_attributes_cache = array();
138+
}
139+
140+
/**
141+
* Gets the total number of cached entries.
142+
*
143+
* Useful for debugging and testing.
144+
*
145+
* @since 6.10.0
146+
*
147+
* @return int The number of cached directive entry sets.
148+
*/
149+
public function get_cache_size(): int {
150+
return count( $this->cache );
151+
}
152+
}
153+

0 commit comments

Comments
 (0)