Skip to content

[TwigComponent] Build the exposed properties without a generator - #3785

Merged
Kocal merged 1 commit into
symfony:3.xfrom
Kocal:perf/twig-component-properties-fast-path
Aug 16, 2026
Merged

[TwigComponent] Build the exposed properties without a generator#3785
Kocal merged 1 commit into
symfony:3.xfrom
Kocal:perf/twig-component-properties-fast-path

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

getProperties() runs once per component render and allocated a Generator plus an iterator_to_array() pass on every call. extractProperties() was private, had a single caller, and that caller materialised it immediately, so nothing was ever lazy.

Build the array directly and drop the generator. Both shapes get faster, and there is no special case for either.

200k calls, component with five public properties and no #[ExposeInTemplate]: ~88 ms -> ~37 ms. Same count on a component that does use the attribute: ~207 ms -> ~162 ms. On a more realistic scale, an admin CRUD page with a table of 50 rows (6 columns, 2 buttons and a badge each, so 501 component renders) goes from 0.24 ms to 0.12 ms per page: a fraction of a millisecond, but for free.

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

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

use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
use Symfony\UX\TwigComponent\ComponentProperties;

// The overwhelmingly common shape: public props, no #[ExposeInTemplate].
class PlainComponent
{
    public string $title = 'Hello';
    public string $type = 'success';
    public bool $dismissible = true;
    public int $count = 3;
    public ?string $icon = null;
}

// The other shape, to check it does not regress.
class ExposingComponent
{
    public string $title = 'Hello';

    #[ExposeInTemplate('label')]
    private string $name = 'ryan';

    public function getName(): string
    {
        return $this->name;
    }

    #[ExposeInTemplate]
    public function computedThing(): string
    {
        return 'computed';
    }
}

$properties = new ComponentProperties(PropertyAccess::createPropertyAccessor());

foreach (['plain' => new PlainComponent(), 'with attributes' => new ExposingComponent()] as $label => $component) {
    $properties->getProperties($component, true);

    $start = hrtime(true);
    for ($i = 0; $i < 20000; ++$i) {
        $properties->getProperties($component, true);
    }
    printf("%-18s -> %8.2f ms\n", $label, (hrtime(true) - $start) / 1e6);
}

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 06:07
@Kocal
Kocal force-pushed the perf/twig-component-properties-fast-path branch 3 times, most recently from 85690ed to d293e70 Compare August 15, 2026 22:21
| Q              | A
| -------------- | ---
| Bug fix?       | no
| New feature?   | no
| Deprecations?  | no
| Documentation? | no
| Issues         | -
| License        | MIT

`getProperties()` runs once per component render and allocated a `Generator`
plus an `iterator_to_array()` pass on every call. `extractProperties()` was
private, had a single caller, and that caller materialised it immediately, so
nothing was ever lazy.

Build the array directly and drop the generator. Both shapes get faster, and
there is no special case for either.

200k calls, component with five public properties and no `#[ExposeInTemplate]`:
~88 ms -> ~37 ms. Same count on a component that does use the attribute:
~207 ms -> ~162 ms.

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

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

use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
use Symfony\UX\TwigComponent\ComponentProperties;

// The overwhelmingly common shape: public props, no #[ExposeInTemplate].
class PlainComponent
{
    public string $title = 'Hello';
    public string $type = 'success';
    public bool $dismissible = true;
    public int $count = 3;
    public ?string $icon = null;
}

// The other shape, to check it does not regress.
class ExposingComponent
{
    public string $title = 'Hello';

    #[ExposeInTemplate('label')]
    private string $name = 'ryan';

    public function getName(): string
    {
        return $this->name;
    }

    #[ExposeInTemplate]
    public function computedThing(): string
    {
        return 'computed';
    }
}

$properties = new ComponentProperties(PropertyAccess::createPropertyAccessor());

foreach (['plain' => new PlainComponent(), 'with attributes' => new ExposingComponent()] as $label => $component) {
    $properties->getProperties($component, true);

    $start = hrtime(true);
    for ($i = 0; $i < 20000; ++$i) {
        $properties->getProperties($component, true);
    }
    printf("%-18s -> %8.2f ms\n", $label, (hrtime(true) - $start) / 1e6);
}
```

Blackfire:

- before — 567ms wall / 563ms CPU: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/cd854c89-f191-48d5-a55c-78dae641ff1d/graph
- after — 460ms wall / 460ms CPU: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/9e415611-5943-4b9c-b6ea-1ed127f69f44/graph
- diff: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/compare/cd854c89-f191-48d5-a55c-78dae641ff1d...9e415611-5943-4b9c-b6ea-1ed127f69f44/graph

Analysis, implementation and benchmarks by Claude Opus 5.
@Kocal
Kocal force-pushed the perf/twig-component-properties-fast-path branch from d293e70 to 4a6a4b5 Compare August 15, 2026 22:56
@Kocal Kocal changed the title [TwigComponent] Skip the generator when a component exposes nothing extra [TwigComponent] Build the exposed properties without a generator Aug 15, 2026
@carsonbot carsonbot added Status: Reviewed Has been reviewed by a maintainer and removed Status: Needs Review Needs to be reviewed labels Aug 16, 2026
@Kocal
Kocal merged commit 6c789ab into symfony:3.x Aug 16, 2026
34 checks passed
@Kocal
Kocal deleted the perf/twig-component-properties-fast-path branch August 16, 2026 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Performance Status: Reviewed Has been reviewed by a maintainer TwigComponent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants