Skip to content

Commit 1d886b3

Browse files
committed
[LiveComponent] Cache the attribute-method lookups per component class
| Q | A | -------------- | --- | Bug fix? | no | New feature? | no | Deprecations? | no | Documentation? | no | Issues | - | License | MIT `liveListeners()`, `preDehydrateMethods()`, `postHydrateMethods()`, `preReRenderMethods()` and `isActionAllowed()` each rebuild a `ReflectionClass` and walk every public method of the component, on every render and every request. What they return only depends on the component class. Memoize the resolved methods per class and attribute. An earlier revision of this branch put the cache in `AsTwigComponent`, where those helpers live, but they have no other consumer than `AsLiveComponent`, so the cache belongs here and TwigComponent is left untouched. Timings use the call counts from the review: a page holding five LiveComponents calls `liveListeners()` and `preDehydrateMethods()` once per component on the initial render, and a single component update calls each of the five once. initial render, 5 components 18.1 us -> 2.9 us one component update 8.2 us -> 1.0 us Sixteen microseconds off a page render is not going to show up in a profile; the ratio is what it is because reflection is simply not needed more than once per class. Benchmarked from the repository root with `symfony php bench.php`: ```php <?php require __DIR__.'/src/LiveComponent/vendor/autoload.php'; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveAction; use Symfony\UX\LiveComponent\Attribute\LiveListener; use Symfony\UX\LiveComponent\Attribute\PostHydrate; use Symfony\UX\LiveComponent\Attribute\PreDehydrate; use Symfony\UX\LiveComponent\Attribute\PreReRender; #[AsLiveComponent('Bench')] class BenchComponent { #[PreDehydrate] public function onPreDehydrate() {} #[PostHydrate] public function onPostHydrate() {} #[PreReRender] public function onPreReRender() {} #[LiveAction] public function save() {} #[LiveListener('foo:bar')] public function onFooBar() {} public function noise1() {} public function noise2() {} public function noise3() {} public function noise4() {} public function noise5() {} public function noise6() {} } $component = new BenchComponent(); $render = static function () use ($component) { for ($i = 0; $i < 5; ++$i) { AsLiveComponent::liveListeners($component); AsLiveComponent::preDehydrateMethods($component); } }; $update = static function () use ($component) { AsLiveComponent::liveListeners($component); AsLiveComponent::preDehydrateMethods($component); AsLiveComponent::postHydrateMethods($component); AsLiveComponent::preReRenderMethods($component); AsLiveComponent::isActionAllowed($component, 'save'); }; foreach (['initial render' => $render, 'one update' => $update] as $label => $fn) { $fn(); $start = hrtime(true); for ($i = 0; $i < 2000; ++$i) { $fn(); } printf("%-16s %6.1f us\n", $label, (hrtime(true) - $start) / 1e3 / 2000); } ``` Analysis, implementation and benchmarks by Claude Opus 5.
1 parent 26a3746 commit 1d886b3

1 file changed

Lines changed: 36 additions & 5 deletions

File tree

src/LiveComponent/src/Attribute/AsLiveComponent.php

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
#[\Attribute(\Attribute::TARGET_CLASS)]
2626
final class AsLiveComponent extends AsTwigComponent
2727
{
28+
/**
29+
* @var array<class-string, array<class-string, \ReflectionMethod[]>>
30+
*/
31+
private static array $methodsPerAttribute = [];
32+
2833
public string $route;
2934
public string $method;
3035
public int $urlReferenceType;
@@ -93,7 +98,7 @@ public function serviceConfig(): array
9398
*/
9499
public static function isActionAllowed(object|string $component, string $action): bool
95100
{
96-
foreach (self::attributeMethodsFor(LiveAction::class, $component) as $method) {
101+
foreach (self::cachedMethodsFor($component, LiveAction::class) as $method) {
97102
if ($action === $method->getName()) {
98103
return true;
99104
}
@@ -111,7 +116,7 @@ public static function isActionAllowed(object|string $component, string $action)
111116
*/
112117
public static function preReRenderMethods(object|string $component): iterable
113118
{
114-
return self::attributeMethodsByPriorityFor($component, PreReRender::class);
119+
return self::cachedMethodsByPriorityFor($component, PreReRender::class);
115120
}
116121

117122
/**
@@ -123,7 +128,7 @@ public static function preReRenderMethods(object|string $component): iterable
123128
*/
124129
public static function postHydrateMethods(object|string $component): iterable
125130
{
126-
return self::attributeMethodsByPriorityFor($component, PostHydrate::class);
131+
return self::cachedMethodsByPriorityFor($component, PostHydrate::class);
127132
}
128133

129134
/**
@@ -135,7 +140,7 @@ public static function postHydrateMethods(object|string $component): iterable
135140
*/
136141
public static function preDehydrateMethods(object|string $component): iterable
137142
{
138-
return self::attributeMethodsByPriorityFor($component, PreDehydrate::class);
143+
return self::cachedMethodsByPriorityFor($component, PreDehydrate::class);
139144
}
140145

141146
/**
@@ -148,12 +153,38 @@ public static function preDehydrateMethods(object|string $component): iterable
148153
public static function liveListeners(object|string $component): array
149154
{
150155
$listeners = [];
151-
foreach (self::attributeMethodsFor(LiveListener::class, $component) as $method) {
156+
foreach (self::cachedMethodsFor($component, LiveListener::class) as $method) {
152157
foreach ($method->getAttributes(LiveListener::class) as $attribute) {
153158
$listeners[] = ['action' => $method->getName(), 'event' => $attribute->newInstance()->getEventName()];
154159
}
155160
}
156161

157162
return $listeners;
158163
}
164+
165+
/**
166+
* @param object|class-string $component
167+
* @param class-string $attribute
168+
*
169+
* @return \ReflectionMethod[]
170+
*/
171+
private static function cachedMethodsFor(object|string $component, string $attribute): array
172+
{
173+
$class = \is_object($component) ? $component::class : $component;
174+
175+
return self::$methodsPerAttribute[$class][$attribute] ??= iterator_to_array(self::attributeMethodsFor($attribute, $component));
176+
}
177+
178+
/**
179+
* @param object|class-string $component
180+
* @param class-string $attribute
181+
*
182+
* @return \ReflectionMethod[]
183+
*/
184+
private static function cachedMethodsByPriorityFor(object|string $component, string $attribute): array
185+
{
186+
$class = \is_object($component) ? $component::class : $component;
187+
188+
return self::$methodsPerAttribute[$class][$attribute] ??= self::attributeMethodsByPriorityFor($component, $attribute);
189+
}
159190
}

0 commit comments

Comments
 (0)