[TwigComponent] Minor performance improvements when using {% props %}#3340
Merged
Kocal merged 1 commit intosymfony:2.xfrom Feb 12, 2026
Merged
[TwigComponent] Minor performance improvements when using {% props %}#3340Kocal merged 1 commit intosymfony:2.xfrom
{% props %}#3340Kocal merged 1 commit intosymfony:2.xfrom
Conversation
There was a problem hiding this comment.
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 useComponentAttributes::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
ComponentAttributesTestto cover removing multiple keys viawithout().
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.
Kocal
commented
Feb 10, 2026
There was a problem hiding this comment.
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.
34b01ef to
4b081f1
Compare
81a76b0 to
d751096
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When running Blackfire on https://github.com/Kocal/Gotta-Catch-Em-All (especially this PR), I noticed that the code generated by
PropsNodecould be slighty optimized.Each (minor) performance improvement are individually commited.
Before:
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 []; }