Opened 4 years ago
Last modified 2 months ago
#56784 new defect (bug)
Optimization in wp_staticize_emoji function
| Reported by: | kac1per | Owned by: | |
|---|---|---|---|
| Priority: | normal | Milestone: | Awaiting Review |
| Component: | Emoji | Version: | 6.0.2 |
| Severity: | normal | Keywords: | has-patch |
| Cc: | Focuses: | performance |
Description
In the wp_staticize_emoji function there is a piece of code which seems to be there to optimize the process, but in fact does the opposite.
The piece i'm writing about:
// Quickly narrow down the list of emoji that might be in the text and need replacing.
$possible_emoji = array();
foreach ( $emoji as $emojum ) {
if ( false !== strpos( $text, $emojum ) ) {
$possible_emoji[ $emojum ] = html_entity_decode( $emojum );
}
}
Feeding all of 3575 emoji enitities into strpos function generates much more overhead than passing all existing emojis for further processing. This can be easily observed by entirely skipping the false !== strpos( $text, $emojum ) check and adding all entities to $possible_emoji array – the whole execution of the wp_staticize_emoji function becomes nearly 10x faster. This effect happened to me every time I tested it no matter the content or length of the passed text.
Change History (4)
#3
@
20 months ago
I ran some tests to compare the old function and the new suggested function, but the improvement wasn’t significant. However, I’m not entirely confident in my testing process, so I believe it would be beneficial to conduct standard and more rigorous testing to move ahead with this issue.
This ticket was mentioned in PR #11942 on WordPress/wordpress-develop by @swapnil1010.
2 months ago
#4
- Keywords has-patch added; needs-patch removed
## What problem does this solve?
Trac ticket: https://core.trac.wordpress.org/ticket/56784
wp_staticize_emoji() runs on RSS/email content via the_content_feed, comment_text_rss, and wp_staticize_emoji_for_email(). After the early ASCII / &#x checks, it builds a $possible_emoji list before the HTML-aware replacement loop.
Since [41701] / #35293, that list is built by looping over every entry in _wp_emoji_list( 'entities' ) (~3,700+ sequences) and calling str_contains( $text, $emojum ) on the full input for each one. The intent was to avoid running the heavier preg_split() / per-chunk replacement path when no emoji entities are present.
In practice, that upfront loop is more expensive than the work it avoids on typical content (long posts with no emoji, or text that contains &#x from non-emoji numeric entities). Each call performs thousands of substring searches over the entire string, and the inner HTML loop already skips entities that are not present in a chunk via str_contains( $content, $emojum ).
Reported impact: skipping only the str_contains guard (and always building the full map) can make the function much faster on common inputs, but that is not a complete fix because we still need:
html_entity_decode()for matched entities (used in the<img alt="…">attribute).- An early return when nothing needs replacing, so we do not run the HTML parsing loop unnecessarily.
## What does this change?
Replace the O(emoji catalog × text length) pre-scan with:
- One
preg_match_all()over the text to find contiguous hex entity sequences:(?:&#x[0-9a-f]+;)+ - O(1) lookup against a static hash map of known emoji entities (
array_fill_keys( _wp_emoji_list( 'entities' ), true )), built once per request html_entity_decode()only for sequences that exist in the emoji entity list
The rest of wp_staticize_emoji() is unchanged: same early exits, same HTML tag handling, same $possible_emoji structure consumed by the inner loop, same output format for feeds and email.
### Complexity comparison
| Step | Before | After |
|---|---|---|
| Find candidates | ~3,700+ str_contains() on full $text | 1 preg_match_all() + iterate unique matches (usually 0–few)
|
| Validate emoji | implicit in str_contains | isset( $emoji_entities[ $emojum ] )
|
Decode for alt | per match (unchanged) | per match (unchanged) |
| Early return | when $possible_emoji empty | when $possible_emoji empty (preserved)
|
### Why not remove the pre-filter entirely?
Removing the pre-filter and only checking entities inside the per-chunk loop would speed up the initial pass but would regress cases where the text contains &#x (or encoded entities) but no emoji from the catalog: we would always pay for preg_split() and the full chunk loop. This patch keeps the early-return behavior while making candidate discovery proportional to entity-like sequences in the content, not the size of the emoji catalog.
## Testing
- [ ] Existing unit tests pass:
`bash npm run test:php -- --filter Tests_Formatting_Emoji
## Use of AI Tools
AI assistance: Yes
Tool(s): ChatGPT
Model(s): GPT-5.5
Used for: PR wording and implementation discussion. Final code and testing were completed manually.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
Thanks for this one, @kac1per.
It seems that the code block you're referencing is there to ensure both the encoded and decoded versions of the emoji are available further down. So only skipping the condition would not be the full solution.
Could you prepare a patch or pull request demonstrating your suggested changes? This will help make it more clear to everyone viewing this ticket and allow the automated tests to run.