Maintainer review of anonrig/router performance, compatibility, and PR claims #8087
tannerlinsley
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
On August 16,
anonrig/routerwas announced as a drop-in replacement for TanStack Router with the same API surface that was "up to 19x faster." A follow-up post said this happened after PRs had been open at TanStack for more than two months. Those posts have now spread into calls for the Router team to act, merge, or port the work.I audited the repository state that produced the announcement, the current repository, the benchmark harness, its copied compatibility tests, and the public PR history.
There are useful ideas here, and Yagiz fixed several problems quickly after I sent him my findings. But the original claims were inaccurate, the first replacement headline repeated the loader-work problem before it was fixed, and the public record does not support the suggestion that TanStack ignored him or his work.
The technical findings and PR outcomes below are tied to public code or history. One section summarizes private communication because a later public post characterized that outreach; I am not publishing the transcripts. If I have a fact, permalink, or reproduction wrong, reply with the specific evidence. I will correct material errors here and record the change at the bottom.
The snapshots reviewed
60e04958d8b2fc22b18029Later fixes matter to the current assessment, but they do not rewrite what was published. Likewise, launch-state failures should not be presented as the current state.
TanStack did not ignore this work
The benchmark claims are one problem. The suggestion that this project happened because TanStack would not communicate or listen is another.
That implication does not match the record. Even setting our private conversations aside, the public GitHub history shows discussion, review, a reproduction request, a direct merge, replacement merges that preserved Yagiz's authorship, and a superseding PR that Yagiz thanked the maintainer for.
At the time of the "more than 2 months" post, the three still-open original PRs were 54 to 56 days old, not more than two months. The pre-announcement record was:
Four of those seven PRs, #7662 through #7665, were performance-focused. They proposed narrow changes to Start's frame decoder, structural-sharing key collection, and default search parsing. None contained the settled-server load shortcut behind the original 19x result or the warm-match cache, parallel load coordinator, and matcher work behind the later navigation headlines. #7664 has limited overlap with the separate implementation at the level of structural-sharing key enumeration, but it does not implement any of those headline mechanisms.
The public table is not the full communication record. In direct messages during this period, Yagiz asked me to review #7661 and #7673. I told him Router and Start were our top priority, linked #7714 as the broader work superseding #7661, acknowledged that the PR trail should've been updated sooner, and offered to continue the technical discussion on GitHub.
Yagiz also asked for me to be added to a shared project Slack. I was added, but no Router rewrite, replacement package, or benchmark proposal was raised with me there. He later wrote, "I've reached out. They are working on Start v1 to continue working on Tanstack Router." The outreach was about reviews for #7661 and #7673, not this rewrite, and my response said Router and Start were our top priority. I am summarizing the sequence because it bears directly on the public claim; I am not publishing private messages.
We should have cleaned up stale original PR statuses sooner. That was poor housekeeping. It was not a refusal to listen, communicate, review, merge, or credit the work.
Since the announcement, Yagiz has split several narrow optimizations into five focused TanStack PRs: #8085 for query-string encoding and decoding, #8086 for
cleanPath, #8089 forparseHref, #8090 forinterpolatePath, and #8091 for SSR match IDs. This is the right reviewable shape, and each should be evaluated on its merits.Those PRs still do not contain the fork's warm-navigation coordinator, settled-load shortcut, or production matcher. They show that individual ideas can be proposed upstream without treating the separate implementation as a patch set.
The original 19x result compared different work
The number itself was reproducible. The conclusion was not.
The 19.80x row repeatedly called
router.load()on the same already-loaded server router. The implementation had a special settled-server return path whose source comment explicitly called out "the Node bench". It returned without rerunning matching or loaders.TanStack's default loader
staleTimeis zero. The fork's headline path skipped that loader work, including after invalidation. The project's own later benchmark-correction commit describes the prior headline as reusing a settled server router and hitting a skip that did not rerun loaders. This showed a settled-server shortcut was fast, but it did not show an equivalent router operation was 19x faster.The rest of the announcement benchmark was a five-route in-memory Node fixture with one trivial synchronous loader. It did not include React rendering, a browser, HTTP, HTML generation, lazy modules, or realistic loader I/O. The announcement's own other rows were much smaller, including 2.90x for warm navigation, 1.91x for cold SSR load, and 1.56x for an empty request handler. Some utility rows were slower than TanStack.
What Yagiz fixed
After I sent him the audit, he made several material corrections:
cc74345removed a process-global SSR serialization cache after a request-isolation defect was reported.9cd1366acknowledged that the prior settled-load headline skipped loader work, changed the benchmark, and fixed invalidation.9c5ff10removed the 20x settled-load headline.b6263deadded stronger server request isolation and made server loads rerun loaders.8d8b2fc.cce6bf8later fixed the replacement navigation benchmark's omitted/defaultstaleTime: 0behavior.167d999added a loader-count guard to the comparison harness and rotated utility inputs so repeated last-value cache hits no longer decide those rows.I reported the SSR request-isolation defect privately. It is fixed in the reviewed current commit, which includes regression coverage. I am intentionally leaving the reproduction details out of this public post.
Those are good fixes. They address the specific launch-state behaviors described above. "It passes now" does not make the announcement-state claims accurate.
The replacement 14x headlines at
8d8b2fcalso compared different workThe first correction snapshot replaced the settled-load headline with 14.09x typed navigation and 14.30x changing-params navigation. Those throughput numbers reproduced, but the warm cache at
8d8b2fctreated an omittedstaleTimeas indefinitely reusable even though TanStack's default is zero.In a fixed-operation probe shaped like that benchmark, typed navigation ran 2 loaders in the fork and 40 in TanStack. Running changing params afterward produced 48 versus 100, and fresh changing-param routers produced 50 versus 100. The benchmark's own warmups populated every repeated destination before timing, so its timed fork loops ran no loaders while TanStack continued rerunning default-stale loaders.
Making
staleTime: 0explicit on both sides equalized loader counts and reversed the local synthetic result. Across eight fresh-process runs, the fork measured 0.52x TanStack for typed navigation and 0.34x for changing params. The probe, output, and commands are available for review.That finding remains accurate for
8d8b2fc. It no longer describes the latest reviewed snapshot.cce6bf8explicitly says the headline rows skipped loaders because omittedstaleTimewas treated as permanent, then fixes that behavior.The current benchmark matches loader counts
I rechecked
22b18029. The fixed-operation probe now reports equal loader calls:anonrig/routerAcross three local Node 24 runs, the revised script reported 15.31x to 16.36x typed navigation and 11.90x to 13.71x changing-params navigation with those equal counts. The current README publishes 16.01x and 15.61x on its Xeon fixture. For this small synchronous-loader fixture, the throughput advantage is reproducible without the prior default-stale loader skip.
This is a material correction and deserves credit. It is still a five-route in-memory Node loop with no browser, React render, HTTP, HTML, lazy modules, or loader I/O. Equal loader counts in that fixture also do not establish equal loader behavior. Targeted differential probes at the same commit found:
anonrig/routerloaderDepschanges frommode=atomode=bdeps: undefined, and did not store the dependency or corresponding data{ mode: "a" }and{ mode: "b" }; final dependency and data were"b"navigaterejected,onErrorwas not called, and no matches were committedonErrorran and the route committed with an error matchThe warm path constructs match IDs without
loaderDeps, its reused loader context omitsdeps, and its Promise branch has no rejection handler. These combinations are not covered by the passing copied suite.The latest snapshot also adds a deliberate history tradeoff. Memory history discards its oldest half at 2,048 entries. After 2,100 pushes followed by
go(-2100), the fork retained 1,077 entries and stopped at/1024; TanStack retained 2,101 and returned to/0. The published history-push row therefore includes bounded work bought by deleting navigable history.At
22b18029, the copied runtime suite passes locally with 163 files, 2,416 passing tests, and one skip. The first-party suite and both vendored type configurations pass, and CI is green. The current README has also replaced its earlier drop-in goal with the more accurate statement that the public names match and the tests are a copied, modified subset."Drop-in" requires behavioral compatibility
At the announcement commit, running the repository's own copied TanStack compatibility suite produced 197 failing test cases across 20 files, plus two unhandled rejections. That is 197 failing cases, not necessarily 197 separate defects, but it was not evidence of a drop-in replacement.
The latest reviewed state is materially better. In my run, all 163 runtime files in its modified copied suite passed, with 2,416 passing tests and one skip.
That is meaningful progress. It is not "all TanStack tests." It is a copied and modified subset of React Router, router-core, and history tests. It does not run TanStack's complete monorepo, integration, build, package, end-to-end, or TypeScript-version matrix. The most detailed matcher suite is also aliased to a compatibility matcher while the benchmarked production router keeps a separate fast matcher.
The latest snapshot fixes the default-stale loader-count mismatch, but the targeted
loaderDeps, context, and error probes above still diverge on the benchmark-eligible warm path. The project also targets a narrower environment, currently React 19.2 and Node 24. Its README reports React Router at 35.8 kB gzip versus TanStack's 29.5 kB, and router core at 30.1 kB versus 21.6 kB, attributing the extra size to its warm path and matcher interners.This is why matching exports and TypeScript signatures are not enough to establish a drop-in replacement. A separate implementation can expose the same API and still be faster, but compatibility is the behavior users rely on, including invalidation, staleness, errors, context, dependencies, matching, SSR isolation, and framework integration.
This is not a patch set TanStack can simply copy
anonrig/routeris a separate implementation behind a similar public API. It is not a collection of isolated patches against TanStack Router, and there is no safe way to copy it wholesale.There is narrow overlap. #7664 covers one structural-sharing key-enumeration optimization, and the five focused post-announcement PRs extract utility changes that can be reviewed independently. The parallel warm coordinator, SSR lanes, history behavior, and production matcher are separate architecture, not an upstream patch series.
Every useful idea still needs to be adapted to TanStack's architecture and proven against equivalent loader behavior, matching, React and Solid integration, SSR isolation, type tests, end-to-end behavior, and bundle size. That is normal engineering work, not resistance to performance improvements.
We will evaluate focused optimizations with:
I want Router faster, and I do not care whose idea gets us there. I do care that we compare the same work and describe the contribution history accurately.
Yagiz, I am asking you to correct the original 19x, drop-in, and PR framing publicly. The repository is materially improved today. The public record should reflect both what you fixed and what was inaccurate when this was announced.
Environment and commands
Current benchmark runs were performed on an Apple M5 Pro, macOS 26.5.2, Node 24.15.0, and pnpm 11.22.0. The repository pins
@tanstack/react-router1.170.29,@tanstack/router-core1.171.24, and@tanstack/history1.162.1 for comparison.The throughput medians above use fresh processes. Loader-call counts come from fixed-operation differential probes using the benchmark's own route shapes.
Deterministic loader-count probe
Save this as
scripts/audit-loader-parity.tsin the current reviewed checkout and run it with the same Node command used by the benchmark:Output at
8d8b2fc:{ "operationsPerRow": 100, "typed": { "ours": 2, "tanstack": 40 }, "changingParamsAfterTyped": { "ours": 48, "tanstack": 100 }, "changingParamsFreshRouter": { "ours": 50, "tanstack": 100 } }Correction log
60e0495and first corrected commit8d8b2fc.22b18029; recorded the default-stale fix and new loader-count guard, updated current benchmark and test results, documented remaining warm-path and history differences, clarified that four pre-announcement PRs were performance-focused without containing the headline mechanisms, updated the five focused follow-up PRs, and summarized the direct communication record without publishing private transcripts.All reactions