I Only Wanted the Tests to Run Faster
A contribution to V8 showed me what open source makes possible, and why shared code can take that further inside a company.
I went looking for the cause of a slow test suite and ended up with a fix merged into V8, the JavaScript engine behind Chrome and Node.js. From my Node bug report to both projects merging changes took about two and a half days. I am still rather excited about that.
For the wider argument, start with The Coordination Tax in Your Software Estate. This experience left me delighted by open-source collaboration and wondering why companies copy the separation that independent projects need, even when they own both sides.
InnerSource brings open-source ways of working inside a company. The code stays private, but colleagues can read it and propose changes across team boundaries. The team responsible still sets the direction, reviews contributions and decides what to merge.
My experience crossed three independently maintained projects. For anyone trying to make InnerSource work between teams in one company, there is plenty to celebrate here, and something to learn about which boundaries we actually need.
The problem kept leading somewhere else
Our tests ran under Vitest, a tool for running automated tests. Vitest, in turn, ran inside Node.js. Node uses V8 to execute JavaScript. A slowdown in the tests could come from any of those layers. I started with the one we used directly.
Vitest already had a known problem with mocks, the functions that stand in for real ones during a test. Andrew Brennan had reported it in January: as a test run continued, mocks accumulated, and clearing their state took longer. Scott Cooper changed Vitest to stop repeatedly scanning mocks that had already been cleared. His change merged on 2 September.
That improvement was in Vitest 5. We were on Vitest 4, so we needed to upgrade or bring the patch back to our version. But there was another question. Our tests had become slower after a Node upgrade. Vitest's repeated cleanup explained the amount of work; it did not explain why the same work now took longer.
I used AI to help investigate that question, and throughout the patch work that followed. I was not a specialist in the engine underneath Node. I still had to choose what to investigate, test the explanations and decide what the evidence justified. I remained responsible for what I submitted.
To separate Vitest's behaviour from Node's, I took the relevant mock code and removed pieces while checking that the slowdown remained. Eventually, a small JavaScript program with no dependencies reproduced it. The program did not need Vitest at all. Changing the Node version was enough to make it fast or slow.
That gave me a much smaller experiment. I compared Node releases and found the jump between 26.3.1 and 26.4.0. Nightly builds narrowed it to a single day's changes. Building Node before and after the suspect change confirmed which change exposed the slowdown.
Then I measured where the extra time went. It was inside V8, the engine executing the program, when it wrote values into objects. V8 keeps internal descriptions of object structure so it can reuse work. In the slow case, objects that had shared those descriptions were getting separate ones.
I traced that loss of reuse to two kinds of object competing for the same entry in a small V8 cache. Each kept displacing the other. Both Node builds contained the same V8 version, but the Node change had altered the engine's initial state so that this particular pair collided. The upgrade had exposed an engine weakness.
Now the two parts made sense together. Vitest was doing unnecessary cleanup, and V8 was making that cleanup more expensive. Reducing the work in Vitest offered a way around the slowdown. Repairing the cache in V8 could also help other code that encountered the same behaviour.
I proposed changing how V8 chose the cache entry and added a regression test. On 13 September, I submitted the V8 patch and documented the reproduction and diagnosis in Node issue #66011. Maintainers could check the reasoning without our application or test suite.
The submission had to earn their time. A plausible explanation and generated code would not have been enough. The reproduction, comparisons between builds and regression test gave them something concrete to assess.
During review, Leszek Swirski suggested using V8's existing hash-combining function. I revised the patch and tested it more widely. I shared the unfavourable results too, so the reviewers could judge the trade-off. Leszek and Jakob Kummerow approved the change, the landing checks ran, and the fix and regression test merged.
The affected loop in the local reproduction went from roughly 50 to 6 nanoseconds per item, about eightfold for that operation. The benefit to an application depends on how much time it spends doing that work.
I had followed a problem into an engine I had previously only used. Its maintainers helped improve the repair and kept the decision over what entered their project. I could contribute without joining their team or taking over their responsibilities.
A small repair in a widely shared engine
Part of the excitement is where the repair can travel. V8 runs JavaScript in Chrome and Node.js. It powers Cloudflare Workers, and Electron bundles it into desktop software, including Visual Studio Code and Slack. The same engine sits underneath remarkably different parts of an ordinary working day.
Those products will not all encounter this particular slowdown. But as they adopt versions containing the fix, affected code can benefit without its authors repeating our investigation. Something that began with our tests has a route into software far beyond our application. I find that wonderful.
Four patches across three layers
Back in our own test suite, I had also ported the Vitest improvement to Vitest 4 and tested it that Sunday, 13 September. A colleague then upgraded us to Vitest 5, so we no longer needed that patch. The version boundary had already created work, even though an upgrade eventually removed our need to carry it.
While the V8 work proceeded, Matteo Collina made a separate change in Node. It avoided the immediate trigger by changing when some internal symbols were created. My V8 patch addressed the cache hash directly.
Someone asked whether the Node change merely moved the problem, leaving it liable to return after a later change. Matteo explained why he was proceeding:
“However we can't assume when the V8 fix is going to land, and this seems better than reverting the ffi change.”
That is a sound decision for an independent project. Node is responsible for its users and cannot set V8's priorities or release schedule. It needed a remedy it could control while the engine change was still being reviewed.
Look across the whole chain and there are four patches across three layers: Scott's Vitest improvement, my Vitest 4 backport, Matteo's Node mitigation and my V8 repair. Vitest reduced the work. Node avoided the trigger. V8 repaired the cache behaviour underneath. The engine defect belonged in V8, but projects above it had good reasons to help their own users sooner.
The Vitest improvement has value of its own; these were four pieces of implementation work rather than four copies of one fix. Each offered a route to relief through a different version or project. The collaboration worked, and the separate release paths still required extra effort.

This is the coordination tax
That extra effort is the coordination tax: getting a shared change through separate versions, release schedules and adoption decisions. The merged V8 repair still has to pass through those steps.
Node can acquire the change through a V8 upgrade or a cherry-pick into an existing release line. A cherry-pick need not wait for a new V8 release, but it still needs review and testing. Node must then ship the resulting runtime and its users must adopt it. Node documents those routes.
Vitest needs no further release for existing tests to benefit from a fixed Node runtime. I have asked whether a Node 26 cherry-pick would be useful; there is no commitment yet. Matteo's mitigation gives Node its own route to relief in the meantime.
This is entirely legitimate in open source. Independent projects serve different consumers, answer to different maintainers and cannot assume everyone will move together. Their freedom to evolve separately is part of what makes the ecosystem work.
One company can make the next step easier
But why recreate that separation between teams when we own the code on both sides? Team ownership does not require a separate package release, version negotiation and upgrade project for every shared repair. For our own interdependent code, that is work we choose to impose on ourselves.
InnerSource gives us the contribution path I experienced here: bring evidence and a proposed improvement, then work with the owners to make it fit. The contributor owes the owning team a considered, tested proposal and the willingness to revise it. Owners bring the wider context and judgement over whether it belongs. That takes review capacity, but it does not require treating teams as independent suppliers.
Consider the internal equivalent: a shared component, a layer that consumes it and an application that exposes the fault. In a monorepo where those consumers build against the same shared source, a contributor can fix the component and test the affected application against that revision. The relevant owners can review the work together.
When the shared fix merges, the next consumer build can contain it. There is no need to publish an intermediate internal package and ask every consuming team to schedule its own upgrade. That can also remove the reason for a temporary implementation downstream while another team prepares its release.
A monorepo has to be operated that way. Putting separately pinned copies and independent integration queues into one directory preserves much of the delay. Builds and release controls remain, and changes across services may need staged deployment. External runtimes still need to be adopted too: moving our application code into a monorepo would not make V8 part of our own release authority.
We should keep an internal release boundary because the software needs it, not simply because another team owns it. Companies can embrace the openness I found in this experience, then use the advantage independent projects do not have: we can change our own shared code and its consumers together. We do not have to organise ourselves as strangers exchanging releases.
I only wanted the tests to run faster. I came away with a small contribution to V8, a great deal of gratitude to its maintainers and less patience for companies that make this harder than it needs to be.