---
title: 'I didn’t expect parsing HTML5 with Symfony Crawler to be THAT slow'
source: 'https://youtube.com/watch?v=bxaYfuROvxs'
video_id: 'bxaYfuROvxs'
date: 2026-08-03
duration_sec: 908
---

# I didn’t expect parsing HTML5 with Symfony Crawler to be THAT slow

> Source: [I didn’t expect parsing HTML5 with Symfony Crawler to be THAT slow](https://youtube.com/watch?v=bxaYfuROvxs)

## Summary

The video discusses a performance issue discovered in a Magento 2 store where parsing HTML5 with Symfony's DOM Crawler took 100 milliseconds, an order of magnitude slower than expected. The presenter, Benjamin, explains how assumptions about code performance can be misleading and demonstrates how to identify and fix such bottlenecks using profiling tools like Tideways.

### Key Points

- **Performance Issue Discovery** [00:03] — HTML5 parsing with Symfony DOM Crawler took 100ms in a Magento 2 store; after fix, it dropped to ~10ms, an order of magnitude faster.
- **Assumptions vs Reality** [00:20] — Developers often misjudge performance due to assumptions; profiling is essential to verify actual bottlenecks.
- **Root Cause Analysis** [01:02] — Tideways profiler showed the link parser from the module 'link preload' was constructing a Symfony DOM Crawler with HTML, taking 100ms.
- **Unexpected Slowness** [01:30] — The assumption was that DOMDocument (a C extension) would be used, taking ~10ms, but the actual parser was Masterminds HTML5, which is slower.
- **Performance Heuristics** [02:01] — Developers rely on experience-based estimates: string ops ~0.001ms, file system ~0.01ms, Redis ~0.1ms, SQL queries 1-100ms depending on indexing.
- **Profiler Deep Dive** [03:23] — The profiler revealed that the actual parsing is done by Masterminds HTML5 parser, not DOMDocument, explaining the slowness.
- **Historical Context** [04:02] — HTML5 support was added to Symfony DOM Crawler in 2019 (PR29306) and made default in Symfony 7.0 (2022, PR44170).
- **Why HTML5 Parser Was Introduced** [04:45] — DOMDocument only parses HTML4, causing bugs with HTML5; Symfony introduced HTML5 parser to fix these, but it's 10x slower.
- **Activation Condition** [05:13] — The HTML5 parser activates when content starts with HTML5 doctype and the useHtml5Parser flag is true (default since 2022).
- **Performance Impact on TTFB** [05:44] — Using the slower parser affects backend performance and Time to First Byte (TTFB), negatively impacting Core Web Vitals.
- **Test Script Setup** [06:43] — A test script bootstraps Magento, uses customer HTML, runs the link parser, and checks preload lines to compare parsers.
- **Profiling with Tideways** [07:27] — Running with Tideways profiler showed the link parser took 67ms on a MacBook Pro M4, still too slow.
- **Disabling HTML5 Parser** [08:29] — By setting useHtml5Parser to false, the crawler uses DOMDocument, reducing time to 139ms vs 193ms (52ms faster).
- **Improvement Confirmation** [09:25] — The crawler construct is 52ms faster, and the link parser is 5x faster with DOMDocument; up to 10x in other scenarios.
- **PHP 8.4 Native HTML5 Parsing** [10:29] — PHP 8.4 adds C-level HTML5 parsing, which can be used to speed up further; requires version checks for Symfony.
- **Version Checks** [11:15] — Check PHP version >= 8.4 and Symfony DOM Crawler version >= 7.4 (PR 61475 merged Aug 2025) to use native HTML5 parsing.
- **Future Compatibility** [12:51] — In Symfony 8.0, the useHtml5Parser parameter will be removed; need to check version to avoid fatal errors.
- **Final Verification** [14:01] — After changes, the link parser is fast, and DOMDocument is used instead of Masterminds, confirming the fix.
- **Conclusion** [14:45] — Assumptions about performance can be off by an order of magnitude; verify with production profilers like Tideways.

### Conclusion

The video emphasizes the importance of profiling to uncover performance bottlenecks that assumptions miss, and provides a practical solution for Symfony DOM Crawler HTML5 parsing slowness.

## Transcript

simple HTML 5 parsing with Symfony DOMC crawler take 100 milliseconds while analyzing a magenta 2 store. The fix in this case brought down the performance to roughly 10 milliseconds in order of magnitude faster. And this makes for a
great story to tell about us developers making assumptions about the performance of code while writing it and then completely misjudging how long it takes. And a usual disclaimer when investigating performance problems, it's
not a matter of assigning blame uh to who introduced the bottleneck. We are here to learn from them for the future. Mo, I am Benjamin and my work is focused on PHP performance topics for the last 10 years, helping thousands of
developers, companies, and open-source projects along the way. As developers, we usually don't have the performance numbers available for every line of code that we write. So we learn to live with horistics and assumptions to do this on
the fly while we write code. These horistic ticked off my alarm bells when analyzing a magenta 2 store. Tideways profiler showed the module link preload profiler showed the module link preload from eo. I came across its link paraser
at link headers from response method and it constructs a symfony dom crawler instance with the HTML of the response instance with the HTML of the response taking 100 milliseconds.
This is entirely unexpected for me as my assumption was that it's using DOM document under the hood a PHP extension that is written in C and even with a very large HTML response it should not take that long maybe up to 10
milliseconds. That is my feeling how long this code should take based on my experience building PHP code for the last 25 years. And because we don't have live profiling running for every line that we write, developers learn in their
career by experience to estimate how much time various functions and algorithms roughly take. Let's say PHP string or array manipulation functions take 0.001 millisecond roughly in my experience. A
file system call 10 times more 0.01 01 milliseconds. Then the next order of magnitude may be some radis call to memory 0.1 milliseconds. And then database queries. Let's say an SQL query with a wear clause based on primary key
1 millisecond. SQL query with a wear clause on different columns maybe some complexity around 10 milliseconds. And then SQL query with a wear clause on columns and no index. let's say 100 milliseconds or even many seconds and so
on and so on. I have so many different assumptions in my mind about how long should things take. They pop up once I write the code. It's like a background process running in my mind evaluating constantly 1
milliseconds here, 100 milliseconds there and so on and so on to get a rough estimate how long a controller, a command or something will run in the end. So the question to me was why is the Symfony domc crawler so slow and you
can quickly find out by going a bit deeper in the stack in the profiler to reveal that the actualist parsing is done by the mastermind's HTML 5 parser
go down here crawler construct crawler at at content at HTML content parse HTML at at content at HTML content parse HTML string crawler parse HTML 5 and then
string crawler parse HTML 5 and then from there we the HTML 5 parse and tokenizer parse is called from the mastermind's name space and the DOM document is not uh actually called at all. This is not how I remember Symfony
DOM crawler working when I used it a few years back and that would prove me right. The optional support for HTML 5based parsing was added in 2019 in PR29306 and it was made the default only with
and it was made the default only with Symfony 7.0 in 2022 with PR44170. used it a lot or didn't need it to be like super fast maybe I came across this
but it wasn't relevant enough and now I saw it for this customer hitting really hard before this change symfony domc crawler would use the PHP extension DOM
document under the hood which only parses HTML 4 compliant and that created a lot of subtle and annoying bugs when parsing HTML L5. And so it makes sense
that uh the Symfony team fixes these passing errors by introducing a HTML 5 paraser. But the problem is when that paser is 10 times slower than the paser is 10 times slower than the original code. And this caused other
people to report issues on the Symfony project pointing this performance change out. This pasa activates itself only when the beginning of the content starts when the beginning of the content starts with the HTML 5 dock type and when the
with the HTML 5 dock type and when the use HTML 5 pasa flag to the constructor use HTML 5 pasa flag to the constructor is true which uh since uh 2022 is the default back to the method add link headers from response. So it doesn't
have to be 100% perfect. we don't need to perfectly add all styles, scripts, to perfectly add all styles, scripts, and images um using a a preload tag. The reason for this is that uh the browser will eventually catch those um anyways
will eventually catch those um anyways parsing HTML 5 and then load them. It might take a little bit longer. However, um using this uh 10 times slower paraser for HTML 5, what we are doing essentially is we are affecting the
backend performance to render the time to first bite. So this is a critical pass in the performance. It affects the Google web vital quite negatively and um
100 milliseconds are just not worth this simple performance optimization. The browser will not catch up this 100 milliseconds. uh it's on its own. So what we can do now is we write ourselves a little test script. I did that here.
So I'm bootstrapping the magenta codebase that's necessary to be able to dependencies. Then I put um HTML code into the uh
Then I put um HTML code into the uh dummy response object. um I use that from the customer that I talked at the beginning from with the 100 milliseconds beginning from with the 100 milliseconds slowdown and then I run the link paraser
and then I check how many lines in the response contain preloading. So this way I see when I change that to run with um something with a different kind of paraser if the result is similar or it doesn't find any results anymore.
So then what I'm doing is I run tight with profiler with profiler and the reason for this is that I want to see the uh performance in the worst case. So let's open that. We go to the
case. So let's open that. We go to the call graph and we look for the link para call graph and we look for the link para parse method. So, it only takes 67 milliseconds here. So, it's faster probably because on my uh MacBook Pro M
probably because on my uh MacBook Pro M uh M4 um uh the code the CPU code doing here is just way faster than on on a server that has maybe more generic um uh hardware. So, still 66 milliseconds are way too
long going through the slowest pass. We still see it's crawler construct related still see it's crawler construct related and um we can go through it and see uh further down it's using the masterminds library. So that is what we wanted to
show. Let's go back to the code and then in Let's go back to the code and then in the crawler we can see we can disable the crawler we can see we can disable use HTML 5 passer. So let's do that.
use HTML 5 passer. So let's do that. use htm. I'm using um named arguments arguments that would otherwise interfere. So I'm doing this change running the profiler again
running the profiler again and then I can directly jump um to compare it against the previous trace trace and I can see it's running much faster.
and I can see it's running much faster. So 139 milliseconds to 193. Let's check in the core graph. The reason for this yeah tokenizer attribute 6,000 calls less. So this is the most the biggest improvement. And the reason for this
improvement. And the reason for this essentially is that crawler construct which is the entry point to the using of the DOM crawler is now 50 um 52
the DOM crawler is now 50 um 52 milliseconds faster. And uh this is like the order of magnitude improvement that we were looking for and we were we were looking for and we were expecting. So um with DOM document
expecting. So um with DOM document uh if we look at the trace uh if we look at the trace and look again for link passer pass we and look again for link passer pass we see it's now um five times faster than
see it's now um five times faster than in the code before. And um for like different setups and scenarios, I've seen this to be 10 times faster as well. seen this to be 10 times faster as well. So it's a very big performance change to
use uh the mastermind's code versus using DOM document. So are we done now? Not really. Let's look at the code again and see if it catches the same amount of links. It does that. So from the feature
perspective, at least for this HTML code, it produces the same response. However, um PHP 8.4 um PHP 8.4 um added support for HTML 5 parsing. So
if we just upgrade that Magento 2 store so to use PHP 8.4, we can make use of C-levelbased HTML 5 parsing and that would make the HTML 5 parsing and that would make the code again much faster. So um what do we
need to do to support that? Uh since the link preload module is generic code, we would uh need to check for the PHP version. So using the PHP para if
PHP version ID is greater is greater um 8.4 four and then we use that flag um 8.4 four and then we use that flag here. And then we should also check that
here. And then we should also check that um Symfony is using a version that supports this change. This happens with the pull request 61475. It was merged uh last month, so in
August 2025, and it will be included in Symphony 7.4 onwards. So we need to change the code to check for that and you can use uh do that using the
composer package installed versions and then you can say get version of Symfony then you can say get version of Symfony DOM crawler and uh
then I need to check that version compare installed versions is bigger than 7.4.
So in this case I will use the HTML 5 parser uh for the crawler and in all the other cases so with lower PHP versions um in the link paraser in this preload
um in the link paraser in this preload module we use the HTML 4-based pasa with all the caveats of the pasa being not as correct as possible. correct as possible. The next problem is that um in
Symphony um 8.0 this parameter will go away. So uh we need to make an additional change here and check that we only do that if
installed version compare installed version get version compare installed version get version from Symphfony crawler is
numbers. It returns one zero or minus one. So if the left one is smaller than one. So if the left one is smaller than the right one, so 8.0000 00 0. In that
the right one, so 8.0000 00 0. In that case, we want to case, we want to um use it with the flag and in the other case we want to or have to use it with the original code
um because otherwise it will create a fatal error because the um argument doesn't exist anymore under that name and the code will not like this. So this way we have it working. Uh let's
So this way we have it working. Uh let's check again. Uh we call tideways run and check again. Uh we call tideways run and we can see the output is still uh much lower than um with in the case without this fix.
um with in the case without this fix. And we can verify in the code that we are running dom document load HTML and we are not using the mastermind's code. So it it constructs it but it's not using the code
it but it's not using the code and um the link parser pass method is also quite fast compared to the slow code we looked at before. This story shows how the assumptions we make about performance can quickly be
off by an order of magnitude or even more and that we should verify them with actual production numbers every once in a while using a production ready profiler such as Tideways. If you like to stay informed about PHP performance
topics, please subscribe to this channel on YouTube or subscribe to the newsletter. The link is in the description. Bye.
