{"slug": "making-react-testing-library-tests-43-faster", "title": "Making React Testing Library Tests 43% Faster", "summary": "Sentry engineer Daniel Griesser used OpenAI's Codex to make React Testing Library tests 43% faster, reducing jsdom 30 runtime from 17.18s to 9.77s on a real Sentry test file, and 21% faster than the current jsdom 26 setup. The changes, merged into jsdom and dom-accessibility-api, optimize label scanning, event paths, and selector handling without altering the tests themselves.", "body_md": "# Making React Testing Library Tests 43% Faster\n\nReact Testing Library’s `getByRole`\n\nis 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`\n\n: 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.\n\nIt’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`\n\nwith `getByTestId`\n\n. No swapping out `userEvent`\n\n. The tests should stay exactly the same while the libraries underneath them get faster.\n\n[The result](#the-result)\n\nI used [a real Sentry test file](https://github.com/getsentry/sentry/blob/c73856753969efc2e12f13363c4db17a3b80849c/static/gsAdmin/components/provisionSubscriptionAction.spec.tsx) built around a large form.\n\n| Setup | Time |\n|---|---|\n| Sentry’s current jsdom 26 setup | 12.41s |\n| jsdom 30 before these changes | 17.18s |\n| With the merged label and event changes | 12.09s |\n| With the DOMSelector fast path too | 9.77s |\n\nTogether, the three library changes made the jsdom 30 version **43% faster**. The final result was also **21% faster than the current jsdom 26 setup**.\n\n[Using Codex](#using-codex)\n\nI started with a vague prompt. I’ve found Sol works well when given a lofty goal:\n\nI need you to find a greater than 20% performance gain in running\n\n`getByRole`\n\non a larger DOM.\n\nCodex 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`\n\nand 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.\n\nAlong the way I had to steer Codex away from:\n\n- treating a microbenchmark win as the final result\n- splitting the test file so Jest could spread it across more workers\n- rewriting the tests to use cheaper queries or interactions\n- hacking up React’s development runtime for a change I could never land\n\nThe change had to speed up the machinery underneath the tests, and it had to belong somewhere I could actually send it.\n\nI 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`\n\n. We traced the behavior past `dom-accessibility-api`\n\n, which only asks the browser for `.labels`\n\n, to the jsdom code doing the repeated scans. That became the first jsdom fix.\n\nFrom 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.\n\nBy 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.\n\nMy 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.\n\n[Stop scanning every label over and over](#stop-scanning-every-label-over-and-over)\n\nThe biggest win came from how jsdom handled `input.labels`\n\n.\n\nTesting Library calculates accessible names when you write something like this:\n\n```\nscreen.getByRole('textbox', { name: 'Email' });\n```\n\nCalculating that name can read the `labels`\n\nproperty for every candidate input. Before [this change](https://github.com/jsdom/jsdom/pull/4237), every input walked the entire DOM root independently to find its labels.\n\nIf a form had 100 controls, jsdom could scan the same DOM 100 times during one query, changing only the control it was looking for.\n\nThe 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`\n\ncollections still behave like they should.\n\nReading the labels for 100 controls went from **60.52ms to 0.67ms**, about **91 times faster**.\n\n[The selector fast path was never fast](#the-selector-fast-path-was-never-fast)\n\njsdom uses [DOMSelector](https://github.com/asamuzaK/domSelector) for selector matching. DOMSelector has a fast path for the selectors it supports in `matches()`\n\n, but jsdom has two JavaScript objects representing the document: its internal implementation object and the public `document`\n\nwrapper.\n\nThe fast-path check compared those two objects with `===`\n\n. They can never be equal. Every supported `matches()`\n\ncall coming from jsdom fell through to the slower general-purpose matcher.\n\n[The fix](https://github.com/asamuzaK/domSelector/pull/309) teaches DOMSelector that the wrapper and implementation object are the same document before doing the comparison.\n\nThe matcher benchmark took **89% less time**. A larger `getByRole('button')`\n\nbenchmark took **42% less time**.\n\nTesting Library and jsdom call `matches()`\n\nall over the place. The fast selector already existed; jsdom just never reached it.\n\n[Events should not keep searching the same path](#events-should-not-keep-searching-the-same-path)\n\nDispatching an event means building a path from the target through its ancestors. jsdom then has to work out the correct `event.target`\n\nat every stop, including shadow DOM cases.\n\nBefore [this change](https://github.com/jsdom/jsdom/pull/4242), every stop searched backward through the event path to find its target. A deeper tree meant a longer path and more repeated searching through that path. jsdom also prepared listener state for elements that did not have a listener for that event.\n\nThe fix records the effective target while building the path, then reads it directly during dispatch. It also skips the listener setup when nothing is listening for that event.\n\nEvent throughput improved by **12% to 36%**, depending on the depth of the tree and how many elements had listeners.\n\nThat matters for React tests because `userEvent`\n\ndoes not dispatch one event and call it a day. A normal interaction can produce a small parade of pointer, mouse, focus, input, and click events. Saving work on every event adds up across a test suite without changing how any listener sees the event.\n\n[What this means for your tests](#what-this-means-for-your-tests)\n\nYour test suite probably will not see the same improvement. This file happened to hit all three hot paths: a large labeled form, lots of semantic Testing Library queries, and plenty of user interactions.\n\nThese changes matter most for tests with:\n\n- large forms with many labels and controls\n- lots of\n`getByRole`\n\nqueries using accessible names - deep rendered DOM trees\n- many\n`userEvent`\n\nor`fireEvent`\n\ninteractions - libraries that make heavy use of\n`matches()`\n\nin jsdom\n\nAs of this post, the [label cache](https://github.com/jsdom/jsdom/pull/4237) and [event-path](https://github.com/jsdom/jsdom/pull/4242) changes have landed in jsdom, but neither is in a published release yet. The [DOMSelector fast-path fix](https://github.com/asamuzaK/domSelector/pull/309) is still open.\n\nThanks to jsdom maintainer [Domenic Denicola](https://github.com/domenic) for reviewing the vibecoded slop and merging both jsdom changes.", "url": "https://wpnews.pro/news/making-react-testing-library-tests-43-faster", "canonical_source": "https://sigh.dev/posts/making-react-testing-library-faster/", "published_at": "2026-08-21 01:40:13+00:00", "updated_at": "2026-08-21 02:13:44.240744+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence"], "entities": ["Sentry", "OpenAI", "Codex", "React Testing Library", "jsdom", "dom-accessibility-api", "Daniel Griesser"], "alternates": {"html": "https://wpnews.pro/news/making-react-testing-library-tests-43-faster", "markdown": "https://wpnews.pro/news/making-react-testing-library-tests-43-faster.md", "text": "https://wpnews.pro/news/making-react-testing-library-tests-43-faster.txt", "jsonld": "https://wpnews.pro/news/making-react-testing-library-tests-43-faster.jsonld"}}