You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while reviewing #100, which moved route gating from wrapper components to loaders. That PR's first description claimed a signed-out visitor to /dashboard no longer downloads the dashboard chunk. That is false, and the claim has been corrected.
A route with a staticloader and a lazy callback runs both concurrently. From react-router@8.3.0, dist/development/lib/router/router.js:2460:
let[value]=awaitPromise.all([runHandler(handler),// the guardlazyHandlerPromise,lazyRoutePromise,// the dynamic import]);
The redirect stops the route rendering. It cannot cancel an import already in flight.
Measured with a probe — a lazy callback that records whether it ran, driven by the real guards:
requireSession: importer ran 1 time(s) (signed-out visitor to /dashboard)
requireAnonymous: importer ran 1 time(s) (signed-in visitor to /login)
So a signed-out visitor deep-linking into a guarded page still pays for its chunk, and a signed-in visitor bounced off /login still pays for the login screen.
The trade, which is why this is an issue and not a fix
The obvious remedy is to sequence the import behind the check: drop lazy, and give the route an element holding a React.lazy screen. The import then fires on first render, which never happens when the loader redirects.
That costs something, and the cost lands on the commoner case. Today the session request and the chunk download run in parallel. Serialising them makes the redirect path cheaper and the successful path slower by one full round trip — and a signed-in visitor is who a guarded route is normally for.
Current waste on a redirect, from pnpm build:
chunk
raw
Dashboard
252 B
PageDetail
321 B
Login
1.7 kB
Signup
1.8 kB
A few hundred bytes to ~2 kB, against a serialised round trip on every successful guarded navigation. On these numbers the trade is bad.
It is bad because the screens are placeholders.Dashboard is essentially a heading. The moment these carry real UI — a table, a chart library, a date picker — the redirect waste grows while the round trip stays the same size, and the trade flips.
What to do
Nothing yet. Revisit when any guarded screen's chunk passes roughly 20 kB, or when a real deployment gives latency numbers to weigh the round trip against.
If it is taken up:
Prefer React.lazy in the element over an eager guard component. The point of Gate routes with loaders instead of wrapper components #100 was to stop mounting a screen in order to decide about it, and a guard component would walk that back.
/login and /signup are the larger chunks and the cheaper fix — an anonymous visitor is the common case there, so sequencing costs the rare path. These four routes do not have to be decided together.
Definition of done
Whichever way it goes, a test that observes whether the importer ran. #100 has no such test: every assertion there observes where the visitor lands, which is why the wrong claim survived review.
Context
Found while reviewing #100, which moved route gating from wrapper components to loaders. That PR's first description claimed a signed-out visitor to
/dashboardno longer downloads the dashboard chunk. That is false, and the claim has been corrected.A route with a static
loaderand alazycallback runs both concurrently. Fromreact-router@8.3.0,dist/development/lib/router/router.js:2460:The redirect stops the route rendering. It cannot cancel an import already in flight.
Measured with a probe — a
lazycallback that records whether it ran, driven by the real guards:So a signed-out visitor deep-linking into a guarded page still pays for its chunk, and a signed-in visitor bounced off
/loginstill pays for the login screen.The trade, which is why this is an issue and not a fix
The obvious remedy is to sequence the import behind the check: drop
lazy, and give the route an element holding aReact.lazyscreen. The import then fires on first render, which never happens when the loader redirects.That costs something, and the cost lands on the commoner case. Today the session request and the chunk download run in parallel. Serialising them makes the redirect path cheaper and the successful path slower by one full round trip — and a signed-in visitor is who a guarded route is normally for.
Current waste on a redirect, from
pnpm build:DashboardPageDetailLoginSignupA few hundred bytes to ~2 kB, against a serialised round trip on every successful guarded navigation. On these numbers the trade is bad.
It is bad because the screens are placeholders.
Dashboardis essentially a heading. The moment these carry real UI — a table, a chart library, a date picker — the redirect waste grows while the round trip stays the same size, and the trade flips.What to do
Nothing yet. Revisit when any guarded screen's chunk passes roughly 20 kB, or when a real deployment gives latency numbers to weigh the round trip against.
If it is taken up:
React.lazyin the element over an eager guard component. The point of Gate routes with loaders instead of wrapper components #100 was to stop mounting a screen in order to decide about it, and a guard component would walk that back./loginand/signupare the larger chunks and the cheaper fix — an anonymous visitor is the common case there, so sequencing costs the rare path. These four routes do not have to be decided together.Definition of done
Whichever way it goes, a test that observes whether the importer ran. #100 has no such test: every assertion there observes where the visitor lands, which is why the wrong claim survived review.