-
-
Notifications
You must be signed in to change notification settings - Fork 681
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: improve performance of Link and routes #1453
Conversation
☁️ Nx Cloud ReportCI is running/has finished running commands for commit f3ba3a1. 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. |
0ac0db2
to
39d4484
Compare
@@ -603,10 +603,10 @@ export function useLoaderData< | |||
...opts, | |||
select: (s) => { | |||
return typeof opts.select === 'function' | |||
? opts.select(s.loaderData) | |||
? opts.select(s.loaderData as TRouteMatch) |
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.
shouldn't this be TRouteMatch['loaderData']
?
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. This is weird. I will have to double check why this was complaining
39d4484
to
cbc692c
Compare
|
||
export type FileRoutePath< | ||
TParentRoute extends AnyRoute, | ||
TFilePath extends string, | ||
TResolvedFilePath = ResolveFilePath<TParentRoute, TFilePath>, | ||
> = TResolvedFilePath extends `_${infer _}` | ||
> = TResolvedFilePath extends `_${string}` |
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 is a simple change but reduces instantiations because the type variable is not used
string, | ||
any | ||
> = TSearchSchemaInput extends SearchSchemaInput | ||
TSearchSchemaUsed = TSearchSchemaInput extends SearchSchemaInput |
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.
For type parameters which act more like variables, its better not to use generic constraints on them. This is because typescript will do eager checking on the value you assign to it.
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 we remove the RouteConstraints
type then fully?
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 want to say constraints are bad because you need them to check generics inferred by the user
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.
But if it's not inferred by the user, I assume it can be trusted
@@ -190,7 +173,6 @@ export class FileRoute< | |||
? undefined | |||
: TLoaderDataReturn, | |||
TChildren extends RouteConstraints['TChildren'] = unknown, | |||
TRouteTree extends RouteConstraints['TRouteTree'] = 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.
I don't think this was needed
@@ -84,67 +84,78 @@ export type RemoveLeadingSlashes<T> = T extends `/${infer R}` | |||
? RemoveLeadingSlashes<R> | |||
: T | |||
|
|||
export type ResolvePaths<TRouteTree extends AnyRoute, TSearchPath> = |
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.
For relative routes starting with '../' and '.' we can narrow the union of the paths down to the descendents of the resolved path.
Basically, because we know TFrom
is a route, we can actually only get descendants of the resolved route. This can make relative pathing faster because we're not dealing with such a large union
TPaths, | ||
TSearchedPaths = SearchPaths<TPaths, TSearchPath>, | ||
> = TSearchedPaths extends string ? `${TTo}/${TSearchedPaths}` : never | ||
> = `${TTo}/${SearchPaths<TRouteTree, TSearchPath>}` |
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 was a silly mistake by me before. Template literal types automatically distribute. Its a small optimization but it does help a bit
@@ -189,13 +200,13 @@ export type ToSubOptions< | |||
// The source route path. This is automatically set when using route-level APIs, but for type-safe relative routing on the router itself, this is required | |||
from?: RoutePathsAutoComplete<TRouteTree, TFrom> | |||
// // When using relative route paths, this option forces resolution from the current path, instead of the route API's path or `from` path | |||
} & CheckPath<TRouteTree, {}, TFrom, TTo> & |
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.
Intersections are slow because we have to merge a union with another union for to
. Better just to change to
prop directly
packages/react-router/src/link.tsx
Outdated
TFrom, | ||
TToParams, | ||
> = TParamVariant extends 'SEARCH' | ||
? Expand<{ |
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.
Think I need to do a few more Expand
@@ -644,30 +713,32 @@ export type LinkProps< | |||
| ((state: { isActive: boolean }) => React.ReactNode) | |||
} | |||
|
|||
type LinkComponentProps<TComp> = React.PropsWithoutRef< |
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 is nice. Basically because TComp
doesn't change on every Link
we can seperate these props into a seperate type and TS will cache this for all Link
's
@@ -14,7 +14,7 @@ export type RoutesById<TRouteTree extends AnyRoute> = { | |||
} | |||
|
|||
export type RouteById<TRouteTree extends AnyRoute, TId> = Extract< | |||
Extract<ParseRoute<TRouteTree>, { id: TId }>, | |||
RoutesById<TRouteTree>[TId], |
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.
Turns out mapped types are faster than Extract
on a union ;). Makes sense when you think about it. But it only shows when you start to have larger code bases
TRouteTree extends AnyRoute = AnyRoute, | ||
TDehydrated extends Record<string, any> = Record<string, any>, | ||
TSerializedError extends Record<string, any> = Record<string, any>, | ||
in out TRouteTree extends AnyRoute = 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.
We can make these invariant to stop TS from doing variance checks on them. I don't think they're necessary in this case
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
export type MakeDifferenceOptional<T, U> = Omit<U, keyof T> & | ||
Partial<Pick<U, keyof T & keyof U>> & | ||
PickRequired<Omit<U, keyof PickRequired<T>>> |
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 why this existed but it works without it?
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 added this here:
92923cc#diff-5fd7a0f05758a78cf8b73c09e4d71d21af63fe3d9b312b0ee75b04041ef078e7R222
do we have a test case that tests if when navigating e.g. from /foo/$bar
to /foo/$bar/baz/$id
, $bar
is optional whereas $id
is not?
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.
Yes. I thought this was covered by the code already here.
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 is the closest test case but maybe I should write a specific one!
test('when navigating from a route to a route with the same params', () => { |
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 would be cool to have that
|
||
params.returns.branded.toEqualTypeOf<{ invoiceId: string }>() |
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 should always Expand
instead of relying on branded
in tests. It means we have complicated intersections
@@ -102,13 +101,12 @@ test('when creating a child route with a loader from the root route with context | |||
const invoicesRoute = createRoute({ | |||
path: 'invoices', | |||
getParentRoute: () => rootRoute, | |||
loader: async () => [{ id: 'invoice1' }, { id: 'invoice2' }] as const, | |||
loader: async (opts) => { | |||
expectTypeOf(opts).toMatchTypeOf<{ context: { userId: string } }>() |
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 decided this approach is better because its testing the inference flow
cbc692c
to
f3ba3a1
Compare
@@ -183,19 +194,19 @@ export type ToSubOptions< | |||
TFrom extends RoutePaths<TRouteTree> | string = string, | |||
TTo extends string = '', | |||
> = { | |||
to?: ToPathOption<TRouteTree, TFrom, TTo> | |||
to?: ToPathOption<TRouteTree, TFrom, TTo> & {} |
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 is a simple way to expand the complex type so it appears nice
TS Performance Improvments
Purpose
Type checking large code bases with many
Route
's andLink
's can get quite sluggish. This PR introduces a number of performance optimizations I've discovered while profiling on the large file based example. The purpose of this PR is to improve suggestion times for such code bases and build timesExtended Diagnostics
On main the following diagnostics are present for the example large file based
After the changes introduced in this PR the instantiations and check time are reduced
Profiling
This is the profile for the large file route on main.
Going deep into each wall of the profile. The biggest wall is the route tree definition. There is a number of things to improve here.
Expand
when necessary and not eagerlyAnd the next significant thing is each
Link
. EachLink
can be improved by the following:LinkComponentProps
to cache props for a component passed tocreateLink
CheckPath
directly on theto
prop to avoid an intersectionparams
andsearch
until the user use these propsAfter the performance improvements the route tree looks more like this
and each
Link
looks like thisLink
may seem like a small improvement for oneLink
but when you have many in large code bases this quickly accumulates