Skip to content

[LiveComponent] Stop materializing the whole call stack twice per component id - #3778

Open
Kocal wants to merge 1 commit into
symfony:3.xfrom
Kocal:perf/live-component-deterministic-id
Open

[LiveComponent] Stop materializing the whole call stack twice per component id#3778
Kocal wants to merge 1 commit into
symfony:3.xfrom
Kocal:perf/live-component-deterministic-id

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

calculateDeterministicId() runs for every live component rendered
without an explicit id. To find the rendering template and its line,
guessTemplateInfo() walked the entire call stack with
debug_backtrace(), then built a Twig\Error\Error purely to capture
that same stack a second time.

Both frames of interest sit near the top of the stack, so read it once
through a growing window (16, then 64, then unbounded) and reuse it for
both lookups. Output is unchanged.

10k ids from a Twig stack nested 40 frames deep: ~88 ms -> ~27 ms.

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

<?php
require __DIR__.'/src/LiveComponent/vendor/autoload.php';

use Symfony\UX\LiveComponent\Twig\DeterministicTwigIdCalculator;
use Twig\Environment;
use Twig\Loader\ArrayLoader;
use Twig\TwigFunction;

$calculator = new DeterministicTwigIdCalculator();

// Nested includes, so the calculator is called from a realistic Twig call stack.
$loader = new ArrayLoader([
    'level3.html.twig' => '{% for i in 1..20 %}{{ deterministic_id() }}{% endfor %}',
    'level2.html.twig' => '{{ include("level3.html.twig") }}',
    'level1.html.twig' => '{{ include("level2.html.twig") }}',
    'page.html.twig' => '{{ include("level1.html.twig") }}',
]);

$twig = new Environment($loader);
$twig->addFunction(new TwigFunction('deterministic_id', static fn (): string => $calculator->calculateDeterministicId()));

// Emulate the depth of a real Symfony request (kernel, controller, listeners...).
$deep = static function (int $depth, callable $fn) use (&$deep) {
    return $depth > 0 ? $deep($depth - 1, $fn) : $fn();
};

for ($i = 0; $i < 60; ++$i) {
    $calculator->reset();
    $deep(40, static fn () => $twig->render('page.html.twig'));
}

Blackfire:

Analysis, implementation and benchmarks by Claude Opus 5.

@Kocal Kocal self-assigned this Aug 15, 2026
@Kocal
Kocal requested review from kbond and smnandre August 15, 2026 05:28
@Kocal
Kocal force-pushed the perf/live-component-deterministic-id branch 2 times, most recently from b2196e7 to 27271c9 Compare August 15, 2026 22:14
…ponent id

| Q              | A
| -------------- | ---
| Bug fix?       | no
| New feature?   | no
| Deprecations?  | no
| Documentation? | no
| Issues         | -
| License        | MIT

`calculateDeterministicId()` runs for every live component rendered
without an explicit id. To find the rendering template and its line,
`guessTemplateInfo()` walked the entire call stack with
`debug_backtrace()`, then built a `Twig\Error\Error` purely to capture
that same stack a second time.

Both frames of interest sit near the top of the stack, so read it once
through a growing window (16, then 64, then unbounded) and reuse it for
both lookups. Output is unchanged.

10k ids from a Twig stack nested 40 frames deep: ~88 ms -> ~27 ms.

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

```php
<?php
require __DIR__.'/src/LiveComponent/vendor/autoload.php';

use Symfony\UX\LiveComponent\Twig\DeterministicTwigIdCalculator;
use Twig\Environment;
use Twig\Loader\ArrayLoader;
use Twig\TwigFunction;

$calculator = new DeterministicTwigIdCalculator();

// Nested includes, so the calculator is called from a realistic Twig call stack.
$loader = new ArrayLoader([
    'level3.html.twig' => '{% for i in 1..20 %}{{ deterministic_id() }}{% endfor %}',
    'level2.html.twig' => '{{ include("level3.html.twig") }}',
    'level1.html.twig' => '{{ include("level2.html.twig") }}',
    'page.html.twig' => '{{ include("level1.html.twig") }}',
]);

$twig = new Environment($loader);
$twig->addFunction(new TwigFunction('deterministic_id', static fn (): string => $calculator->calculateDeterministicId()));

// Emulate the depth of a real Symfony request (kernel, controller, listeners...).
$deep = static function (int $depth, callable $fn) use (&$deep) {
    return $depth > 0 ? $deep($depth - 1, $fn) : $fn();
};

for ($i = 0; $i < 60; ++$i) {
    $calculator->reset();
    $deep(40, static fn () => $twig->render('page.html.twig'));
}
```

Blackfire:

- before — 151ms wall / 124ms CPU / 4.03MB: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/4d22039d-d050-49d2-b425-5711efd903b5/graph
- after — 81ms wall / 81ms CPU / 2.21MB: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/c8121275-ffaf-4667-a3ac-da8e4cc31cdb/graph
- diff: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/compare/4d22039d-d050-49d2-b425-5711efd903b5...c8121275-ffaf-4667-a3ac-da8e4cc31cdb/graph

Analysis, implementation and benchmarks by Claude Opus 5.
@Kocal
Kocal force-pushed the perf/live-component-deterministic-id branch from 27271c9 to 458bbf3 Compare August 15, 2026 22:21
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