Making React Testing Library Tests 43% Faster • sigh.dev - Scott Cooper's dev blogMaking React Testing Library Tests 43% Faster<br>August 20, 2026•Scott Cooper
View more blogs with the tag javascript View more blogs with the tag testing View more blogs with the tag performance
React Testing Library’s getByRole is the correct way to test a form. It checks that fields have the roles and accessible names a user relies on, so a passing test tells me the form is at least minimally accessible and labeled correctly. That takes more work than querySelector: it has to find candidates, work out their implicit roles, filter inaccessible elements, and calculate accessible names. On a large DOM, that can be a lot of work.
It’s Sentry’s annual HackWeek, and alongside my more standard project I wanted to burn some GPT-5.6 Sol tokens on something useful. I started poking at one expensive React test file to see how fast I could make it without rewriting it. No replacing getByRole with getByTestId. No swapping out userEvent. The tests should stay exactly the same while the libraries underneath them get faster.
The result
I used a real Sentry test file built around a large form.
SetupTimeSentry’s current jsdom 26 setup12.41sjsdom 30 before these changes17.18sWith the merged label and event changes12.09sWith the DOMSelector fast path too9.77s<br>Together, the three library changes made the jsdom 30 version 43% faster . The final result was also 21% faster than the current jsdom 26 setup .
Using Codex
I started with a vague prompt. I’ve found Sol works well when given a lofty goal:
I need you to find a greater than 20% performance gain in running getByRole on a larger DOM.
Codex came back with an 81% microbenchmark win from indexing implicit roles by tag. Great, except the benchmark was basically designed around the code it had just made faster. I had it patch the change directly into Sentry’s node_modules and run it there. It did nothing. I then asked how much time the file actually spent inside role queries. The answer was less than 1% of the runtime, so even an 81% improvement there was not going to matter.
Along the way I had to steer Codex away from:
treating a microbenchmark win as the final result
splitting the test file so Jest could spread it across more workers
rewriting the tests to use cheaper queries or interactions
hacking up React’s development runtime for a change I could never land
The change had to speed up the machinery underneath the tests, and it had to belong somewhere I could actually send it.
I pointed Codex at the subscription form test instead. This one spent about 29% of its time in role queries. Profiling led to jsdom rescanning the document for input.labels. We traced the behavior past dom-accessibility-api, which only asks the browser for .labels, to the jsdom code doing the repeated scans. That became the first jsdom fix.
From there I kept Sentry and each library in separate checkouts. Codex patched Sentry’s installed dependencies for quick A/B tests. If an idea survived in Sentry, it moved into the repository that owned the code and got its own tests and benchmark. We repeated that loop for the event-path and selector fixes.
By the second pull request, I had Codex read the feedback maintainers had left on earlier changes to the same files. We used that to check whether the code matched the repository’s patterns, whether the benchmark only showed the best case, and which correctness cases the tests needed to cover. That produced smaller changes, broader benchmarks, and better tests before opening the pull request.
My job was to make it prove each win in a real test, kill the weak ideas, and keep asking where the fix should actually live.
Stop scanning every label over and over
The biggest win came from how jsdom handled input.labels.
Testing Library calculates accessible names when you write something like this:
screen.getByRole('textbox', { name: 'Email' });<br>Calculating that name can read the labels property for every candidate input. Before this change, every input walked the entire DOM root independently to find its labels.
If a form had 100 controls, jsdom could scan the same DOM 100 times during one query, changing only the control it was looking for.
The fix builds one label-to-control index for the current root and shares it between all the controls. When the DOM changes, jsdom throws the index away and rebuilds it the next time someone needs it. The live labels collections still behave like they should.
Reading the labels for 100 controls went from 60.52ms to 0.67ms , about 91 times faster .
The selector fast path was never fast
jsdom uses DOMSelector for selector matching. DOMSelector has a fast path for the selectors it supports in matches(), but jsdom has two JavaScript objects representing the document: its internal implementation object and the public document wrapper.
The fast-path check compared those two objects with ===. They can never be equal. Every...