-
-
Notifications
You must be signed in to change notification settings - Fork 649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
perf: geting lazy?, doing less work? generate more (plus some recommendations) #1564
Conversation
☁️ Nx Cloud ReportCI is running/has finished running commands for commit 236d63c. As they complete they will appear below. Click to see the status, the terminal output, and the build insights. 📂 See all runs for this CI Pipeline Execution ✅ Successfully ran 2 targetsSent with 💌 from NxCloud. |
Still some work needs to be done so not ready just yet |
addChildren = <const TNewChildren extends ReadonlyArray<AnyRoute>>( | ||
addChildren = < | ||
const TNewChildren extends | ||
| Record<string, AnyRoute> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this new object syntax be documented?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added to the performance recommendations
we should add that |
Definitely. Think we need to write a section on it so they can go to it and be able to resolve common problems |
TSearchSchemaUsed = TSearchSchemaInput extends SearchSchemaInput | ||
? Omit<TSearchSchemaInput, keyof SearchSchemaInput> | ||
: TSearchSchema, | ||
TSearchSchemaInput = {}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realized these constraints were not necessary because they were just constrained to {}
which is already extremely loose
@@ -138,78 +144,142 @@ const JestedLayoutB3LayoutC2BarRoute = JestedLayoutB3LayoutC2BarImport.update({ | |||
declare module '@tanstack/react-router' { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@schiller-manuel I've added conditions for the tests in the generator for fullPath
, id
, path
. Are there any situations missing here that I should add?
) | ||
|
||
return `'${filePathId}': { | ||
id: '${id}' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need both id
and path
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought id
was slightly different?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct me if I'm wrong, but you are explicitly setting these values on the declaration so you don't need to resolve the inference again via the TS-resolve types, yes?
Edit: Ignore this comment, I didn't properly read the PR description.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, just want to confirm that we don't end up in a scenario that a created route configuration (not route type declaration) doesn't ever have both an id
and a path
.
Like:
const fooRoute = fooImport.update({
id: 'foo',
path: '/foo'
})
This only affects the type that's printed for module declaration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if I get your point. I think file based routing always had id
and path
in the types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check more into this, it looks good.
Just wanted to make sure we weren't modifying anything in route configuration, and only touching stuff on the type declaration.
TRouteContext | ||
>, | ||
TRouterContext extends RouteConstraints['TRouterContext'] = AnyContext, | ||
TAllParams = ResolveAllParamsFromParent<TParentRoute, TParams>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit faster to name all complex types
Don't merge yet. Need to double check some things. Ideas 💡 |
@SeanCassiere in light of #1584, can you please review this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have the best understanding of all the ts-stuff we do with the router, but trying it out on my machine with out examples, the functionality works identical to what it was like before.
Left some comments, but nothing too crazy, all-in-all amazing, amazing, amazing, work @chorobin 🙌🏼!
) | ||
|
||
return `'${filePathId}': { | ||
id: '${id}' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct me if I'm wrong, but you are explicitly setting these values on the declaration so you don't need to resolve the inference again via the TS-resolve types, yes?
Edit: Ignore this comment, I didn't properly read the PR description.
) | ||
|
||
return `'${filePathId}': { | ||
id: '${id}' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, just want to confirm that we don't end up in a scenario that a created route configuration (not route type declaration) doesn't ever have both an id
and a path
.
Like:
const fooRoute = fooImport.update({
id: 'foo',
path: '/foo'
})
This only affects the type that's printed for module declaration.
export type ParsePathParams<T extends string> = keyof { | ||
[K in Trim<Split<T>[number], '_'> as K extends `$${infer L}` | ||
? L extends '' | ||
export type ParsePathParams< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment may not really belong here at this line, rather it's something I noticed about the link component when testing out the PR.
In the "kitchen-sink-file-based" example, using a <Link>
component it accepts the following to
values.
/route-group
route-group
1
works as expected, but 2
then says it requires params.
Property 'params' is missing in type '{ to: "route-group"; }' but required in type 'MakeRequiredPathParams<Router<Route<any, "/", "/", string, "__root__", RootSearchSchema, RootSearchSchema, RootSearchSchema, RootSearchSchema, ... 10 more ..., { ...; }>, TrailingSlashOption, Record<...>, Record<...>>, string, "route-group">'.ts(2741)
Also, something else, I noticed is that the from
property doesn't accept just route-group
, only allows /route-group
unlike the to
property shown above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, its a separate issue I'm going to be fixing in a separate PR. Its a bug on main as well.
@@ -73,38 +73,65 @@ const LayoutLayout2LayoutARoute = LayoutLayout2LayoutAImport.update({ | |||
declare module '@tanstack/react-router' { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll need to generate the route-trees on all the file-based examples as well and start migrating all the code-based examples over to the object-syntax.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
while I agree we should re-generate the route-trees of file based examples, I don't think we MUST migrate the code based ones.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's slow down a bit. I'm exploring another idea that I really hope means we can not introduce a new signature. But we will see! I'm not sure if it'll work yet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. Turns out the idea is again limited by the children being tuples, it likely forcing unnecessary instantiations...
We need to keep the signature to allow objects for children
<Link from={from} to='..' /> | ||
``` | ||
|
||
### Consider using the object syntax of `createChildren` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this should be addChildren
Also, we probably want to using the |
Trying out the changes from my previous PR on this branch yielded no collisions or regressions that I could observe on that specific layout route behaviour. That being said, @chorobin once the merge conflict has been resolved, you'd want to rebase and confirm the tests I introduced in that PR still work. |
|
||
## Performance Recommendations | ||
|
||
As your application scales, typescript check times will naturally increase. There are a few things to keep in mind when your application scales to keep your TS check times down. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typescript => TypeScript
<Link search={{ page: 0 }} /> | ||
``` | ||
|
||
**These examples are bad for TS performance**. Thats because `search` resolves to a union of all `search` params for all routes and TS has to check whatever you pass to the `search` prop against this potentially big union. As your application grows, this check time will increase linearly to number of routes and search params. We have done our best to optimise for this case (typescript will typically do this work once and cache it) but the initial check against this large union is expensive. This also applies to `params` and other API's such as `useSearch`, `useParams`, `useNavigate` etc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thats => That's
packages/react-router/src/index.tsx
Outdated
@@ -71,6 +64,7 @@ export { | |||
type UseLinkPropsOptions, | |||
type ActiveLinkOptions, | |||
type LinkProps, | |||
type LinkComponentProps, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we really want to export this type? what would this be used for?
I've never heard of this. In conventional commits I thought you had to explicitly mention |
this can be seen here: https://github.com/TanStack/config/blob/main/src/publish/index.js#L144
|
> = ToSubOptions<TRouter, TFrom, TTo> & { | ||
> = ToSubOptions<TRouter, TFrom, TTo> & MaskOptions<TRouter, TMaskFrom, TMaskTo> | ||
|
||
export interface MaskOptions< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naming all of these types helps TS cache stuff when passing LinkProps
to a Link
. Slightly more optimised
- add type tests for objects with children - add runtime support for objects with children - add runtime tests for objects with children
c772e8b
to
cd8d216
Compare
Getting lazy? Doing less work? and generating more (plus some recommendations)
This PR focuses more on complex routes and to some degree many routes. When I say a complex route, I mean routes which have
search
,params
,loaders
and external dependencies such aszod
or other heavy TS related dependencies.A bigger issue I've noticed in TSR is in order to get suggestions, the route tree needs to be evaluated. If your routes contain complex things that TS needs to infer like validation schemas then this can slow down TS evaluating the route tree because its hard at work on the types of the validation libraries. But for most suggestions, we only need the
fullPath
, we don't need to work out the search schema or the params in order to get suggestions forLink
for example untilsearch
is used. The ideal scenario is we do as little as possible in order to get the suggestions we needShow me the results!
I've updated our large filed based example to include:
These include external dependencies which also do TS inference. Note the results may vary depending on what dependencies you use and how the types are written
On main:
On current branch:
Getting lazy? Doing less work?
In TSR one of the largest performance bottlenecks is the route tree building. This node is important to reduce because it is directly linked to TS server suggestion times. What is contributing to this node being so slow is firstly, a lot of work upfront to evaluate
search
,loader
,context
and the others, which are possibly inferred from external libraries.The first interesting change this PR introduces is not a breaking one but introducing a new signature to
addChildren
.addChildren
accepts a tuple of routes like the following:route.addChildren([childRoute1, childRoute2])
addChildren
also in addition can now accept an objectroute.addChildren({ childRoute1, childRoute2 })
.Why this change? Well, it looks like tuples seem to be forcing TS to be doing something in each route which is quite slow and it gets worse with different external dependencies, where objects seem to be more cheap or perhaps more lazy in nature, maybe there's also caching involved? Depending on the libraries used in for example
validateSearch
, you may notice a difference in performance improvements.For example, using
valibot
instead ofzod
in the above example withaddChildren
that accept a tupleNow with
addChildren
that accepts an objectThat's a big difference between tuples and objects. I'm still not totally sure why tuples behave this way or if I'm even correct about my assumptions but to me it looks like tuples are forcing some kind of extra work the TS compiler is doing and its related to
validateSearch
and how the types of the external library are likely written.Profile
The first link that is checked by TS has to evaluate the route tree and it looks like this with
addChildren
accepting a tupleThe same node with
addChildren
accepting an object looks like thisBut thats not the end of the story. Each individual Link is also faster with children as an object. Each Link is checked with the following time when
addChildren
accepts a tupleAnd each Link is checked with the following time with
addChildren
accepts an objectAs mentioned before. 30ms might not seem like much. But if you have 30ms for every
Link
in your code base, it quickly accumulates. 4ms is a huge difference in these casesIn this PR I have switched file based routing over to using objects automatically for a default performance boost. Code based can opt into the signature.
Other stuff
Generating more
File based routing previously has done a lot of string interpretation on the type level in order to calculate things like
fullPath
,path
,id
etc. Template literal types are expensive for the compiler and generally in file based routing, these strings can be generated directly by the generator without hurting inference. This speeds up creating routesImprove params parsing
A more direct implementation of path params parsing without any additional splitting or mapped types
What can blow up your TS Performance
With TSR you want to narrow to the most relevant routes. This means using
from
andto
onLink
or other API's.For example,
<Link search={{ page: 0 }} />
will increase the type checking time on this link because TSR will resolvesearch
to all possible routes (a union of all search params) and will therefore spend longer type checking it. This depends on the size of your route tree and how many search params you have but on our large example, I noticed a singleLink
would type check in roughly 100ms.It is better to narrow the route as much as you can
<Link from={Route.fullPath} search={{ page: 0 }} />
withfrom
orto
. In this case the time type checkingLink
went down to 9ms.This same logic also applies to
LinkOptions
. If you're using types from TSR which are by default not narrowed to a route then this can also blow up TS performance when merging this props withLink
. Just remember that whenfrom
orto
is not specified then things can start slowing down, so definitely try to do it as little as possible.