#Our Monster API Type & Horrible Performance

23 messages · Page 1 of 1 (latest)

autumn rover
#

Our repo is making the type checker in VS code chug hard. Usually 10s before showing type errors or any Intellisense. Often it simply breaks & we have to restart the TS server. It is a larger code base with ~800 react components.

I’ve noticed it usually becomes much slower as soon as I import & call our useAPIQuery hook within a component. This is basically a wrapper around react-query.

I can use it like

const productsQuery = useAPIQuery('products', { searchParams/pathParams: (typed to products GET) })

The call signature is a union of about 160 types from different GET endpoints

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck The call queries[queryStr](params[0]) takes much longer to type-check as the number of queries grows

import queries, { APIQueryStr } from './queries'

const useAPIQuery = <T extends APIQueryStr>(
  queryStr: T,
  ...params: Parameters<(typeof queries)[T]>
): ReturnType<Pick<typeof queries, T>[T]> => queries[queryStr](params[0])

export default useAPIQuery

I love how easy to use this is! The response/request/param types are magically given just providing a single queryStr like ‘products’, ‘organizations’, etc. However there is a huge performance impact with this approach.

Is there any way I can keep this interface externally in our application code but rework the types in a more efficient manner?

marble isle
#

this seems equally good in terms of DX (if not better, because autosuggest):

const productsQuery = apiQueries.useProducts({ searchParams/pathParams: (typed to products GET) })

would that work?

#

i guess that's really just re-exporting your queries object:

const productsQuery = queries.products({ searchParams/pathParams: (typed to products GET) })
autumn rover
#

Thanks for the response! Holy macaroni, that idea never even crossed my mind! I agree I like that DX of directly using queries better. The former option too so it's transparent these are hooks & to use as such.

It seems the performance issue is actually with queries itself as this change made clear.

When hovering over queries I see

(alias) const queries: {
    reminderPresets: (params?: Pick<{
        pathParams: {
            organizationId: string;
        };
        body: never;
        searchParams?: {
            ...blah blah
        } | undefined;
    }, "searchParams" | "pathParams"> & {
        ...; // these are just custom config options
    }) => UseQueryResult<...>;
    ... 163 more ...;
    vitalSettings: (params?: Pick<...> & {
        ...;
    }) => UseQueryResult<...>;
}
import queries

Is a type this big just unavoidably going to cause sluggishness?

Here's an example of a key defined in queries (special case where we override the searchParams type for ergonomics):

{
...
  reminderPresets: vitalsQueryConstructor<
    '/organizations/{organizationId}/reminder-presets',
    BulkRemindersSearchParams
  >('reminderPresets', '/organizations/{organizationId}/reminder-presets', {
    mappers: {
      searchParams: ({ take = 100, searchTerm }) => [
        ['take', take],
        ['searchTerm', searchTerm],
        ['include', ['organization']],
      ],
    },
  }),

normal example:

  orderActivity: ordersQueryConstructor(
    'orderActivity',
    '/orders/{orderId}/activity',
    { cachingStrategy: 'real-time' }
  ),

The queryServiceConstructor is a hideous, complicated mess of code I wrote 2 years ago. Once this type is generated for queries it wouldn't continue to cause performance issues would it?

Tempted to spin up another editor to see if this is just a VS code issue or really a TS issue. I can't find any material on measuring VS code performance. Is there a way to profile the TS type checker in VS code?

marble isle
dusky bolt
autumn rover
#

I've run the tracing & fixed the few hotspots related to some recursive types. Now it looks like the attachment. running tsc --extendedDiagnostics looks like

Files:                         3795
Lines of Library:             29751
Lines of Definitions:        349236
Lines of TypeScript:         245060
Lines of JavaScript:              0
Lines of JSON:                13509
Lines of Other:                   0
Identifiers:                 699651
Symbols:                    2664739
Types:                       715510
Instantiations:            10197456
Memory used:               2083678K
Assignability cache size:    559336
Identity cache size:         152844
Subtype cache size:           29211
Strict subtype cache size:    71624
I/O Read time:                0.71s
Parse time:                   1.54s
ResolveModule time:           0.58s
ResolveTypeReference time:    0.01s
Program time:                 3.10s
Bind time:                    0.67s
Check time:                  25.77s
printTime time:               0.00s
Emit time:                    0.00s
Total time:                  29.54s

Does this seem about right for this project size? Gonna read closer the portions on editor perf

dusky bolt
#

Looks like an awful number of instantiations to me.

#

The "check time" is very high too... it's affecting both vscode and build-time (both use the compiler)

#

But, maybe it's normal for a repo with 800 components :/

dusky bolt
#

Any progress on performance? If so, what did you end up doing? (I feel bad for asking but I'm genuinely interested about TS compiler performance)

autumn rover
#

Np! I've been making slow progress jumping between this & other stuff. I believe we have all the compilerOptions set for optimal performance. I added incremental: true. While nice for rerunning npx tsc I haven't seen any improvement in editor experience.

{
  "compilerOptions": {
    "target": "ESNext",
    "lib": ["dom", "dom.iterable", "esnext"],
    "baseUrl": "src",
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "incremental": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "noFallthroughCasesInSwitch": true,
    "downlevelIteration": true,
    "types": ["cypress", "@testing-library/cypress", "jest", "vite/client"],
    "paths": {
      "@mui/styled-engine": ["./node_modules/@mui/styled-engine-sc"]
    }
  },
  "files": ["cypress.config.ts"],
  "include": [
    "src",
    "cypress/plugins",
    "cypress/support",
    "api-dev",
    "api-utils",
    "workbench",
    "vite.config.ts"
  ],
}

My next step will be turning off all other VS code extensions & taking a TSServer log.

I've been reading up on Project References too to see how/if we could implement that

autumn rover
#

Disabled all extensions. It does feel a little snappier but still sluggish. Looking at a brief tsserver.log 2 different types of commands seem common culprits, semantic classification & code fixes:

Perf 1916 [13:34:19.442] 21::encodedSemanticClassifications-full: elapsed time (in milliseconds) 1822.3282

Perf 1929 [13:34:21.951] 25::getCodeFixes: elapsed time (in milliseconds) 2173.1593

On a more complicated component:

Perf 2067 [14:00:45.730] 73::getCodeFixes: elapsed time (in milliseconds) 5065.4434

Not really sure what actionable information there is here though...
Going to try enableTracing on TSServer next.

dusky bolt
#

@late verge/analyze-trace couldn't give you a few hotspots? 😦

autumn rover
#

I kinda discarded those results because no hotspot was more than 2ish seconds on 30s build. But now I wonder if a hotspot was a bedrock part of our type system it would have a continual impact on TSServer operations?

Only clear red flag is this union of types:

├─ Check file /users/__/documents/projects/addi/src/utils/api/api.ts (2380ms)
│  └─ Check variable declaration from (line 222, char 7) to (line 224, char 2) (1063ms)
│     └─ Check expression from (line 222, char 22) to (line 224, char 2) (1063ms)
│        └─ Compare types 12373 and 12134 (1038ms)
│           ├─ {"id":12373,"kind":"Union","count":238,"types":[ids...]}
│           │  ├─ {"id":12135,"kind":"AnonymousType","location":{"path":"/users/__/documents/projects/addi/src/types/api/api.d.ts","line":14,"char":48}}

Interesting... the referenced AnonymousType at the end is a utility type UnionToIntersection<U> . We call it like

autumn rover
dusky bolt
#

Those types could maybe be optimized but because they're broken, I cannot even attempt it.

autumn rover
#

The intermediate/internal types used to build the final types are broken yes. However notice OrgsAPIResponse & OrgsAPIRequest are correctly created if you hover over them.

Yes I could go & add Extract<keyof AllMyGenerics😭, string (or HTTPMethods)> everywhere since technically the generic could include additional non-string, non-HTTPMethod properties as a more specific type.

And yes I could add an ungodly amount of conditionals making certain that each URL/HTTPMethod/parameters/content/'application/json' property access actually is a keyof the generic type.

But based on my current (admittedly very limited) understanding of TS & category theory this is all to enforce absolute object/property type safety. As long as my input paths type obeys my assumptions it works out ok right?

Sorry for that rant & really thanks a ton for helping me man. I've just lost so much hair and brain cells to Typescript 🤪.

I added an example path with body & searchParams. Now we can test every happy path in this playground by making sure the output (OrgsAPIResponse/OrgsAPIRequest) types are correctly built.
https://tinyurl.com/mrxyzhmb

dusky bolt
#

Will try to play with it today, but from what I'm seeing, there's definitely room for improvement.

dusky bolt
#

Sorry for the link shortener... the result was too large for Discord.

autumn rover
#

This basically worked as a plug n play replacement!! All I added was Record<string, unknown> & in 2 places so the conditional is still true if the extracted property isn't there but other properties are:
https://tsplay.dev/mLr24w

What a beautiful solution! So simple in hindsight, but to use the conditional to grab the desired fields with infer is so nice 😍.

This completely fixed the hotspot & has immediately improved editor snappiness 1000%. I can't even begin express my appreciation...