Repository navigation
[Icons] Cache the rendered HTML of repeated icons in IconRenderer - #4047
Merged
Merged
Conversation
Kocal
force-pushed
the
perf/icons-render-memoize
branch
2 times, most recently
from
October 6, 2026 22:19
e61453d to
87fe874
Compare
| Q | A
| -------------- | ---
| Bug fix? | no
| New feature? | no
| Deprecations? | no
| Documentation? | no
| Issues | -
| License | MIT
Pages render the same few icons over and over, for instance the same action icons in every table row. `IconRenderer::renderIcon()` redid all the work on each call: merging the default, icon set and passed attributes, validating them twice through `Icon::withAttributes()`, adding `aria-hidden`, then escaping every attribute in `Icon::toHtml()`, although the resulting HTML only depends on the icon name and the passed attributes.
`IconRenderer` now keeps the rendered HTML, keyed by `serialize([$name, $attributes])`, and returns it on the next identical call. The key keeps value types apart (`true`, `'1'` and `1` render differently). Calls with a non-scalar attribute value skip the cache, `Icon::withAttributes()` rejects them anyway. The cache is cleared on kernel reset and capped at 1,000 entries, so a process that never resets cannot grow it without bound.
- standalone benchmark below (290 icons per request, 50 distinct, 3 interleaved runs each, identical output): **243-254 ms** before, **72-76 ms** after (3.3x)
- replay of the 414 icons rendered by one request of an admin dashboard (90 distinct): **2.74-2.82 µs** per icon before, **1.00-1.03 µs** after
- per request, `Icon::toHtml()` goes from 414 calls to 90 and `Icon::withAttributes()` from 528 to 122
- on that page it saves about **0.7 ms** per request (~1.5%)
```php
<?php
// Run from src/Icons: php bench.php
use Symfony\UX\Icons\Icon;
use Symfony\UX\Icons\IconRenderer;
use Symfony\UX\Icons\Tests\Util\InMemoryIconRegistry;
require getcwd().'/vendor/autoload.php';
// A table of 25 rows with the same 10 action icons each, plus 40 icons rendered once: 290 renders, 50 distinct.
$calls = $icons = [];
foreach (range(1, 25) as $row) {
foreach (['eye', 'pencil', 'copy', 'share', 'trash', 'star', 'clock', 'check', 'message', 'more'] as $name) {
$calls[] = ['lucide:'.$name, ['class' => 'size-4', 'data-icon' => 'inline-start']];
}
}
foreach (range(1, 40) as $i) {
$calls[] = ['lucide:icon-'.$i, ['class' => 'size-4']];
}
foreach ($calls as [$name]) {
$icons[$name] = new Icon('<path d="M12 2a10 10 0 1 0 0 20a10 10 0 1 0 0-20Zm0 4v6l4 2"/>', ['viewBox' => '0 0 24 24']);
}
$registry = new InMemoryIconRegistry($icons);
$html = '';
$times = [];
for ($run = 0; $run < 7; ++$run) {
$start = hrtime(true);
for ($request = 0; $request < 300; ++$request) {
$renderer = new IconRenderer($registry, ['fill' => 'currentColor', 'height' => '1em', 'width' => '1em']);
foreach ($calls as [$name, $attributes]) {
$svg = $renderer->renderIcon($name, $attributes);
if (0 === $run && 0 === $request) {
$html .= $svg;
}
}
}
$times[] = (hrtime(true) - $start) / 1e6;
}
sort($times);
printf("300 requests of %d icons: median %.0f ms (min %.0f, max %.0f) | sha1 %s\n", count($calls), $times[3], $times[0], $times[6], sha1($html));
```
Blackfire profiles (replay of the dashboard's icons):
- before: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/998268ae-9de5-47ad-929e-e7ee986cb208/graph
- after: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/ecbc2344-85fc-4097-969c-257a4c823423/graph
- comparison: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/compare/998268ae-9de5-47ad-929e-e7ee986cb208...ecbc2344-85fc-4097-969c-257a4c823423/graph
This dashboard is available at https://github.com/Kocal/sf-ux-perfs-twig-components.
Kocal
force-pushed
the
perf/icons-render-memoize
branch
from
October 7, 2026 05:39
87fe874 to
5f9ff05
Compare
Kocal
requested review from
kbond and
smnandre
and removed request for
smnandre
October 8, 2026 05:36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pages render the same few icons over and over, for instance the same action icons in every table row.
IconRenderer::renderIcon()redid all the work on each call: merging the default, icon set and passed attributes, validating them twice throughIcon::withAttributes(), addingaria-hidden, then escaping every attribute inIcon::toHtml(), although the resulting HTML only depends on the icon name and the passed attributes.IconRenderernow keeps the rendered HTML, keyed byserialize([$name, $attributes]), and returns it on the next identical call. The key keeps value types apart (true,'1'and1render differently). Calls with a non-scalar attribute value skip the cache,Icon::withAttributes()rejects them anyway. The cache is cleared on kernel reset and capped at 1,000 entries, so a process that never resets cannot grow it without bound.Icon::toHtml()goes from 414 calls to 90 andIcon::withAttributes()from 528 to 122Blackfire profiles (replay of the dashboard's icons):
This dashboard is available at https://github.com/Kocal/sf-ux-perfs-twig-components.