Skip to content

[TwigComponent] Reduce per-character work in the pre-lexer scan loops - #3776

Open
Kocal wants to merge 1 commit into
symfony:3.xfrom
Kocal:perf/twig-component-prelexer-scan-loop
Open

[TwigComponent] Reduce per-character work in the pre-lexer scan loops#3776
Kocal wants to merge 1 commit into
symfony:3.xfrom
Kocal:perf/twig-component-prelexer-scan-loop

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

consumeUntilEndBlock() ran six substr() comparisons on every character of a block body, and the main loop ran a preg_match() per character to test for whitespace.

Guard the delimiter comparisons behind the only three characters that can start one ({, <, #), and swap the regex for ctype_space().

This only kicks in for templates where a component contains a traditional {% block %}; consumeUntilEndBlock() is never reached otherwise. Only 9 of this repository's 390 Twig templates match, so it doesn't move a whole-repo aggregate, but on the ones that do, the gain holds at realistic sizes too:

206 B    46.6 us -> 24.7 us
322 B    79.3 us -> 29.5 us
728 B     194 us -> 45.8 us

1.9 KB 520 us -> 92.3 us

On the synthetic 167 KB template used in the benchmark below, pre-lexing goes from ~64 ms to ~16 ms. That gain only shows up at compile time though: Twig compiles a template once and caches the result, so this is a cache-warmup and dev-loop win, not something you'll see on every request, as smnandre pointed out during review.

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

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

use Symfony\UX\TwigComponent\Twig\TwigPreLexer;

$body = str_repeat("    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>\n", 40);
$chunk = "<twig:Card>\n    {% block header %}\n{$body}\n    {% endblock %}\n</twig:Card>\n\n";

$input = str_repeat($chunk, 60); // ~167 KB
(new TwigPreLexer())->preLexComponents($input);

Blackfire:

Analysis, implementation and benchmarks by Claude Opus 5.

@Kocal Kocal self-assigned this Aug 15, 2026
@carsonbot carsonbot added Bug Bug Fix Deprecation Documentation Improvements or additions to documentation Feature New Feature TwigComponent Status: Needs Review Needs to be reviewed labels Aug 15, 2026
@Kocal Kocal added Performance and removed Bug Bug Fix Documentation Improvements or additions to documentation Feature New Feature Deprecation labels Aug 15, 2026
@Kocal
Kocal requested review from kbond and smnandre August 15, 2026 05:27
@Kocal
Kocal force-pushed the perf/twig-component-prelexer-scan-loop branch 3 times, most recently from b18e909 to 01e7e47 Compare August 15, 2026 22:21
| Q              | A
| -------------- | ---
| Bug fix?       | no
| New feature?   | no
| Deprecations?  | no
| Documentation? | no
| Issues         | -
| License        | MIT

`consumeUntilEndBlock()` ran six `substr()` comparisons on every character
of a block body, and the main loop ran a `preg_match()` per character to
test for whitespace.

Guard the delimiter comparisons behind the only three characters that can
start one (`{`, `<`, `#`), and swap the regex for `ctype_space()`.

Pre-lexing a 167 KB template made of components with blocks goes from
~64 ms to ~16 ms.

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

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

use Symfony\UX\TwigComponent\Twig\TwigPreLexer;

$body = str_repeat("    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>\n", 40);
$chunk = "<twig:Card>\n    {% block header %}\n{$body}\n    {% endblock %}\n</twig:Card>\n\n";

$input = str_repeat($chunk, 60); // ~167 KB
(new TwigPreLexer())->preLexComponents($input);
```

Blackfire:

- before — 1061ms wall / 1056ms CPU: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/f4c9789b-4a34-43d6-bca9-87e67832a3f0/graph
- after — 73ms wall / 72ms CPU: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/3d2c3cc2-f4c1-471c-aa3d-1246f8514429/graph
- diff: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/compare/f4c9789b-4a34-43d6-bca9-87e67832a3f0...3d2c3cc2-f4c1-471c-aa3d-1246f8514429/graph

Analysis, implementation and benchmarks by Claude Opus 5.
@Kocal
Kocal force-pushed the perf/twig-component-prelexer-scan-loop branch from 01e7e47 to c6eade1 Compare August 15, 2026 22:56
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