Skip to content

[Map] Render each UX icon once instead of once per marker - #3779

Open
Kocal wants to merge 1 commit into
symfony:3.xfrom
Kocal:perf/map-icon-render-cache
Open

[Map] Render each UX icon once instead of once per marker#3779
Kocal wants to merge 1 commit into
symfony:3.xfrom
Kocal:perf/map-icon-render-cache

Conversation

@Kocal

@Kocal Kocal commented Aug 15, 2026

Copy link
Copy Markdown
Member
Q A
Bug fix? no
New feature? no
Deprecations? no
Documentation? no
Issues -
License MIT

Markers on a map almost always share the same icon, but
AbstractRenderer::getMapAttributes() called UxIconRenderer::render()
for every single marker: a registry lookup, two attribute-validation
passes and an HTML build, repeated identically thousands of times.

Memoise inside UxIconRenderer on the arguments it receives, so the cache
key cannot drift from them and every map of the request shares it. The
generated payload is unchanged.

Rendering a Leaflet map: 1000 markers ~9.7 ms -> ~6.5 ms, 5000 markers
~53 ms -> ~37 ms.

Benchmarked from the repository root with blackfire run symfony php bench.php:

<?php
require __DIR__.'/src/Map/vendor/autoload.php';
require __DIR__.'/src/Map/src/Bridge/Leaflet/vendor/autoload.php';

use Symfony\UX\Icons\Icon as UxIconsIcon;
use Symfony\UX\Icons\IconRegistryInterface;
use Symfony\UX\Icons\IconRenderer;
use Symfony\UX\Map\Bridge\Leaflet\Renderer\LeafletRenderer;
use Symfony\UX\Map\Icon\Icon;
use Symfony\UX\Map\Icon\UxIconRenderer;
use Symfony\UX\Map\Map;
use Symfony\UX\Map\Marker;
use Symfony\UX\Map\Point;
use Symfony\UX\StimulusBundle\Helper\StimulusHelper;

$registry = new class implements IconRegistryInterface {
    public function get(string $name): UxIconsIcon
    {
        return new UxIconsIcon(
            '<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/>',
            ['viewBox' => '0 0 24 24', 'fill' => 'none', 'stroke' => 'currentColor', 'stroke-width' => '2'],
        );
    }
};

$renderer = new LeafletRenderer(
    new StimulusHelper(null),
    new UxIconRenderer(new IconRenderer($registry, ['fill' => 'currentColor'])),
);

$map = new Map(center: new Point(48.86, 2.35), zoom: 12);
for ($i = 0; $i < 2000; ++$i) {
    $map->addMarker(new Marker(
        position: new Point(48.86 + $i / 10000, 2.35 + $i / 10000),
        title: 'Marker '.$i,
        icon: Icon::ux('lucide:map-pin'),
    ));
}

$renderer->renderMap($map);

Blackfire:

Analysis, implementation and benchmarks by Claude Opus 5.

@Kocal Kocal self-assigned this Aug 15, 2026
@carsonbot carsonbot added Map Status: Needs Review Needs to be reviewed labels Aug 15, 2026
@Kocal
Kocal requested review from kbond and smnandre August 15, 2026 05:29
@Kocal
Kocal force-pushed the perf/map-icon-render-cache branch 3 times, most recently from ab29fdc to d35e1c3 Compare August 15, 2026 22:21
| Q              | A
| -------------- | ---
| Bug fix?       | no
| New feature?   | no
| Deprecations?  | no
| Documentation? | no
| Issues         | -
| License        | MIT

Markers on a map almost always share the same icon, but
`AbstractRenderer::getMapAttributes()` called `UxIconRenderer::render()`
for every single marker: a registry lookup, two attribute-validation
passes and an HTML build, repeated identically thousands of times.

Memoise inside `UxIconRenderer` on the arguments it receives, so the cache
key cannot drift from them and every map of the request shares it. The
generated payload is unchanged.

Rendering a Leaflet map: 1000 markers ~9.7 ms -> ~6.5 ms, 5000 markers
~53 ms -> ~37 ms.

Benchmarked from the repository root with `blackfire run symfony php bench.php`:

```php
<?php
require __DIR__.'/src/Map/vendor/autoload.php';
require __DIR__.'/src/Map/src/Bridge/Leaflet/vendor/autoload.php';

use Symfony\UX\Icons\Icon as UxIconsIcon;
use Symfony\UX\Icons\IconRegistryInterface;
use Symfony\UX\Icons\IconRenderer;
use Symfony\UX\Map\Bridge\Leaflet\Renderer\LeafletRenderer;
use Symfony\UX\Map\Icon\Icon;
use Symfony\UX\Map\Icon\UxIconRenderer;
use Symfony\UX\Map\Map;
use Symfony\UX\Map\Marker;
use Symfony\UX\Map\Point;
use Symfony\UX\StimulusBundle\Helper\StimulusHelper;

$registry = new class implements IconRegistryInterface {
    public function get(string $name): UxIconsIcon
    {
        return new UxIconsIcon(
            '<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/>',
            ['viewBox' => '0 0 24 24', 'fill' => 'none', 'stroke' => 'currentColor', 'stroke-width' => '2'],
        );
    }
};

$renderer = new LeafletRenderer(
    new StimulusHelper(null),
    new UxIconRenderer(new IconRenderer($registry, ['fill' => 'currentColor'])),
);

$map = new Map(center: new Point(48.86, 2.35), zoom: 12);
for ($i = 0; $i < 2000; ++$i) {
    $map->addMarker(new Marker(
        position: new Point(48.86 + $i / 10000, 2.35 + $i / 10000),
        title: 'Marker '.$i,
        icon: Icon::ux('lucide:map-pin'),
    ));
}

$renderer->renderMap($map);
```

Blackfire:

- before — 182ms wall / 169ms CPU / 8.95MB: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/59ffb3aa-97d0-47a7-8f39-8ba8d2f0e807/graph
- after — 79ms wall / 79ms CPU / 8.36MB: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/91beef39-f210-460d-b43f-8244a3700e20/graph
- diff: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/compare/59ffb3aa-97d0-47a7-8f39-8ba8d2f0e807...91beef39-f210-460d-b43f-8244a3700e20/graph

Analysis, implementation and benchmarks by Claude Opus 5.
@Kocal
Kocal force-pushed the perf/map-icon-render-cache branch from d35e1c3 to 3291457 Compare August 15, 2026 22:56
@Kocal Kocal changed the title [Map] Render each UX icon once per map instead of once per marker [Map] Render each UX icon once instead of once per marker Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants