Skip to content

[6.x] Faster YAML parsing - #15838

Merged
jasonvarga merged 9 commits into
statamic:6.xfrom
ryanmitchell:fast-yaml-parser
Nov 10, 2026
Merged

jasonvarga merged 9 commits into
statamic:6.xfrom
ryanmitchell:fast-yaml-parser

Conversation

@ryanmitchell

Copy link
Copy Markdown
Contributor

Parsing YAML is one of the biggest costs of building the Stache. symfony/yaml parses nested blocks recursively: for each nested block it collects that block's lines and parses them again with a new parser. Every line is scanned once per level of nesting, which makes deeply nested content like Bard slow, and memory-hungry.

This PR adds Statamic\Yaml\FastParser, which Yaml::parse() now uses. It walks the lines once, descending into each nested block as it meets it.

  • Keys and values: plain keys and the most common values are read directly. Any other key or value goes to symfony's own Inline parser, so it's interpreted exactly as symfony would interpret it.
  • Literal blocks: | and |- blocks are read directly. Anything awkward is handed to symfony as a small document of its own.
  • Fallback: anything outside the subset it understands, including all invalid YAML, makes it give up. The whole document is then parsed by symfony/yaml as before, so errors, messages and line numbers are always symfony's.
  • Only reading changes. Dumping is unchanged.

Built together with @DanielDarrenJones.

Speed

symfony/yaml Fast parser Faster
Cold Stache build, 2,000 Bard entries (whole paths() build) 2,226ms 607ms 3.7x
Parse only, 3,284 files from a real site (9.5MB) 1,557ms 186ms 8.4x
Parse only, 300 Bard-heavy entries (4KB, 192 lines each) 258ms 23.5ms 11x

Peak memory while parsing

symfony/yaml Fast parser Change
Real site, average per file 38.4KB 29.4KB −23%
Real site, largest file 861KB 408KB −53%
Bard-heavy entry (4KB) 82.8KB 48.8KB −41%
One 2MB document 28.0MB 18.3MB −35%

The parsed result is identical, so memory held after parsing is unchanged; these are the temporary peaks during the parse. On the real site, 99.4% of files took the fast path, and the rest fell back to symfony/yaml at its normal speed.

Correctness

No differences from symfony/yaml 8.1.5 were found in any of these, including invalid YAML, where both must throw the same error:

Test set Inputs Result
symfony/yaml's own ParserTest, routed through the fast parser 376 tests All pass
Official yaml-test-suite (data-2022-01-17) 402 documents 0 differences
Content from two real sites 3,284 + 5,096 files 0 differences
Differential fuzzing: random and mutated documents, including invalid YAML ~10M documents 0 differences
Statamic's test suite 11,591 tests All pass

Cases the fuzzing found are deliberately left to symfony. These include an unindented list after a blank line, malformed flow collections like [1{2], deep nesting (symfony applies its own limit), and a symfony/yaml 8.1 quirk where a nested | block followed by one last line of front matter loses its final newline.

The new FastParserTest compares every result with symfony's rather than with hard-coded values. It covers hand-picked edge cases, a check that Statamic-dumped content stays on the fast path, and seeded fuzz tests that compare against whichever symfony/yaml version is installed.

Notes

  • The parser caches parsed keys for the life of the process, capped at 2,000 entries so long-running processes (Octane, queue workers) don't grow without limit.
  • Benchmarks were run on macOS with PHP 8.4.25 and symfony/yaml 8.1.5, taking the fastest of 5 runs.

🤖 Generated with Claude Code

ryanmitchell and others added 9 commits November 10, 2026 12:22
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Symfony writes an indentation indicator (e.g. |2-) when a multi-line value's first line starts with spaces, which previously made the whole document fall back to Symfony. These blocks are now handed to Symfony on their own, like |+ blocks already are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Symfony treats a form feed or Unicode whitespace (such as a non-breaking space) after a dash as a sequence separator, so the fast parser could return different data or skip Symfony's errors for hand-edited content containing them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 36 character value of hex digits and dashes with a dash at offset 8 could still be numeric to Symfony, such as 1234567e-123456789012345678901234567, which it reads as a float.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both cases used the same provider key, so the first was overwritten and never ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jasonvarga
jasonvarga merged commit 5b60554 into statamic:6.x Nov 10, 2026
61 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants