Skip to content

[TwigComponent] Minor performance improvements when using {% props %}#3340

Merged
Kocal merged 1 commit intosymfony:2.xfrom
Kocal:twig-component-imp-props-perf
Feb 12, 2026
Merged

[TwigComponent] Minor performance improvements when using {% props %}#3340
Kocal merged 1 commit intosymfony:2.xfrom
Kocal:twig-component-imp-props-perf

Conversation

@Kocal
Copy link
Member

@Kocal Kocal commented Feb 10, 2026

Q A
Bug fix? no
New feature? no
Deprecations? no
Documentation? no
Issues Fix #...
License MIT

When running Blackfire on https://github.com/Kocal/Gotta-Catch-Em-All (especially this PR), I noticed that the code generated by PropsNode could be slighty optimized.

Each (minor) performance improvement are individually commited.

Before:

<?php
    protected function doDisplay(array $context, array $blocks = []): iterable
    {
        $macros = $this->macros;
        // line 1
        $propsNames = [];        if (isset($context['__props']['isLogged'])) {
        $componentClass = isset($context['this']) ? get_debug_type($context['this']) : "";
        throw new \Twig\Error\RuntimeError('Cannot define prop "isLogged" in template "components/Pokemon.html.twig". Property already defined in component class "'.$componentClass.'".');
        }
        $propsNames[] = 'isLogged';
        $context['attributes'] = $context['attributes']->remove('isLogged');
        if (!isset($context['isLogged'])) {            throw new \Twig\Error\RuntimeError("isLogged should be defined for component components/Pokemon.html.twig.");
        }
        if (isset($context['__props']['pokemon'])) {
        $componentClass = isset($context['this']) ? get_debug_type($context['this']) : "";
        throw new \Twig\Error\RuntimeError('Cannot define prop "pokemon" in template "components/Pokemon.html.twig". Property already defined in component class "'.$componentClass.'".');
        }
        $propsNames[] = 'pokemon';
        $context['attributes'] = $context['attributes']->remove('pokemon');
        if (!isset($context['pokemon'])) {            throw new \Twig\Error\RuntimeError("pokemon should be defined for component components/Pokemon.html.twig.");
        }
        if (isset($context['__props']['caughtPokemons'])) {
        $componentClass = isset($context['this']) ? get_debug_type($context['this']) : "";
        throw new \Twig\Error\RuntimeError('Cannot define prop "caughtPokemons" in template "components/Pokemon.html.twig". Property already defined in component class "'.$componentClass.'".');
        }
        $propsNames[] = 'caughtPokemons';
        $context['attributes'] = $context['attributes']->remove('caughtPokemons');
        if (!isset($context['caughtPokemons'])) {            throw new \Twig\Error\RuntimeError("caughtPokemons should be defined for component components/Pokemon.html.twig.");
        }
        $attributesKeys = array_keys($context['attributes']->all());
        foreach ($context as $key => $value) {
            if (in_array($key, $attributesKeys) && !in_array($key, $propsNames)) {
unset($context[$key]);
            }
        }
        // line 2
        if ((($tmp = ($context["isLogged"] ?? null)) && $tmp instanceof Markup ? (string) $tmp : $tmp)) {
            // line 3
            yield "<a
    href=\"";
            // line 4
            yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape($this->extensions['Symfony\Bridge\Twig\Extension\RoutingExtension']->getPath("app_toggle_pokemon_caught", ["pokemonId" => CoreExtension::getAttribute($this->env, $this->source, ($context["pokemon"] ?? null), "id", [], "any", false, false, false, 4)]), "html", null, true);
            yield "\"
    onclick=\"this.querySelector('img').classList.toggle('grayscale-100');\"
>
    ";
        }
        // line 8
        yield "    <img
        src=\"";
        // line 9
        yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape($this->extensions['Symfony\Bridge\Twig\Extension\AssetExtension']->getAssetUrl((("images/pokemon/" . CoreExtension::getAttribute($this->env, $this->source, ($context["pokemon"] ?? null), "id", [], "any", false, false, false, 9)) . ".png")), "html", null, true);
        yield "\"
        alt=\"";
        // line 10
        yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, ($context["pokemon"] ?? null), "name", [], "any", false, false, false, 10), "html", null, true);
        yield "\"
        title=\"";
        // line 11
        yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, ($context["pokemon"] ?? null), "name", [], "any", false, false, false, 11), "html", null, true);
        yield " #";
        yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, ($context["pokemon"] ?? null), "id", [], "any", false, false, false, 11), "html", null, true);
        yield " (";
        yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, ($context["pokemon"] ?? null), "getCatchableInGamesLabel", [], "method", false, false, false, 11), "html", null, true);
        yield ")\"
        class=\"select-none ";
        // line 12
        yield ((CoreExtension::getAttribute($this->env, $this->source, ($context["caughtPokemons"] ?? null), CoreExtension::getAttribute($this->env, $this->source, CoreExtension::getAttribute($this->env, $this->source, ($context["pokemon"] ?? null), "id", [], "any", false, false, false, 12), "toInt", [], "any", false, false, false, 12), [], "array", true, true, false, 12)) ? ("") : ("grayscale-100"));
        yield " w-full place-self-center\"
    />
    ";
        // line 14
        if ((($tmp = ($context["isLogged"] ?? null)) && $tmp instanceof Markup ? (string) $tmp : $tmp)) {
            // line 15
            yield "</a>
";
        }
        yield from [];
    }

After:

    {
        $macros = $this->macros;
        // line 1
        $propsNames = ['isLogged', 'pokemon', 'caughtPokemons'];
        $context['attributes'] = $context['attributes']->without(...$propsNames);
        if (isset($context['__props']['isLogged'])) {
            $componentClass = isset($context['this']) ? get_debug_type($context['this']) : "";
            throw new \Twig\Error\RuntimeError('Cannot define prop "isLogged" in template "components/Pokemon.html.twig". Property already defined in component class "'.$componentClass.'".');
        }
        if (!isset($context['isLogged'])) {        
            throw new \Twig\Error\RuntimeError("isLogged should be defined for component components/Pokemon.html.twig.");            
        }        
        if (isset($context['__props']['pokemon'])) {
            $componentClass = isset($context['this']) ? get_debug_type($context['this']) : "";
            throw new \Twig\Error\RuntimeError('Cannot define prop "pokemon" in template "components/Pokemon.html.twig". Property already defined in component class "'.$componentClass.'".');
        }
        if (!isset($context['pokemon'])) {        
            throw new \Twig\Error\RuntimeError("pokemon should be defined for component components/Pokemon.html.twig.");            
        }        
        if (isset($context['__props']['caughtPokemons'])) {
            $componentClass = isset($context['this']) ? get_debug_type($context['this']) : "";
            throw new \Twig\Error\RuntimeError('Cannot define prop "caughtPokemons" in template "components/Pokemon.html.twig". Property already defined in component class "'.$componentClass.'".');
        }
        if (!isset($context['caughtPokemons'])) {        
            throw new \Twig\Error\RuntimeError("caughtPokemons should be defined for component components/Pokemon.html.twig.");            
        }        
        foreach ($context['attributes']->all() as $key => $value) {
unset($context[$key]);
        }
        // line 2
        if ((($tmp = ($context["isLogged"] ?? null)) && $tmp instanceof Markup ? (string) $tmp : $tmp)) {
            // line 3
            yield "<a
    href=\"";
            // line 4
            yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape($this->extensions['Symfony\Bridge\Twig\Extension\RoutingExtension']->getPath("app_toggle_pokemon_caught", ["pokemonId" => CoreExtension::getAttribute($this->env, $this->source, ($context["pokemon"] ?? null), "id", [], "any", false, false, false, 4)]), "html", null, true);
            yield "\"
    onclick=\"this.querySelector('img').classList.toggle('grayscale-100');\"
>
    ";
        }
        // line 8
        yield "    <img
        src=\"";
        // line 9
        yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape($this->extensions['Symfony\Bridge\Twig\Extension\AssetExtension']->getAssetUrl((("images/pokemon/" . CoreExtension::getAttribute($this->env, $this->source, ($context["pokemon"] ?? null), "id", [], "any", false, false, false, 9)) . ".png")), "html", null, true);
        yield "\"
        alt=\"";
        // line 10
        yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, ($context["pokemon"] ?? null), "name", [], "any", false, false, false, 10), "html", null, true);
        yield "\"
        title=\"";
        // line 11
        yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, ($context["pokemon"] ?? null), "name", [], "any", false, false, false, 11), "html", null, true);
        yield " #";
        yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, ($context["pokemon"] ?? null), "id", [], "any", false, false, false, 11), "html", null, true);
        yield " (";
        yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(CoreExtension::getAttribute($this->env, $this->source, ($context["pokemon"] ?? null), "getCatchableInGamesLabel", [], "method", false, false, false, 11), "html", null, true);
        yield ")\"
        class=\"select-none ";
        // line 12
        yield ((CoreExtension::getAttribute($this->env, $this->source, ($context["caughtPokemons"] ?? null), CoreExtension::getAttribute($this->env, $this->source, CoreExtension::getAttribute($this->env, $this->source, ($context["pokemon"] ?? null), "id", [], "any", false, false, false, 12), "toInt", [], "any", false, false, false, 12), [], "array", true, true, false, 12)) ? ("") : ("grayscale-100"));
        yield " w-full place-self-center\"
    />
    ";
        // line 14
        if ((($tmp = ($context["isLogged"] ?? null)) && $tmp instanceof Markup ? (string) $tmp : $tmp)) {
            // line 15
            yield "</a>
";
        }
        yield from [];
    }

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to improve runtime performance of TwigComponent templates using the {% props %} tag by optimizing the PHP code generated by PropsNode, with a small accompanying test enhancement for multi-key attribute removal.

Changes:

  • Update PropsNode::compile() to precompute prop name lists and use ComponentAttributes::without(...$propsNames) to remove prop attributes in one operation.
  • Change the context-cleanup loop to iterate attribute keys directly (instead of scanning the full context).
  • Extend ComponentAttributesTest to cover removing multiple keys via without().

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/TwigComponent/src/Twig/PropsNode.php Alters generated PHP for {% props %} to reduce per-prop operations and change how context keys are cleaned up.
src/TwigComponent/tests/Unit/ComponentAttributesTest.php Adds coverage ensuring without() supports removing multiple keys in one call.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Kocal Kocal force-pushed the twig-component-imp-props-perf branch from 34b01ef to 4b081f1 Compare February 10, 2026 18:18
Copy link
Member

@kbond kbond left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool!

@carsonbot carsonbot added Status: Reviewed Has been reviewed by a maintainer and removed Status: Needs Review Needs to be reviewed labels Feb 10, 2026
@Kocal Kocal force-pushed the twig-component-imp-props-perf branch from 81a76b0 to d751096 Compare February 12, 2026 19:55
@Kocal Kocal merged commit f3dd98f into symfony:2.x Feb 12, 2026
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

Comments