TypeScript: tsc --watch initial build 3x slower than tsc

TypeScript Version: 3.7.0-dev.20191011, 3.6.4, 3.5.2

Search Terms: DeepReadonly slow watch mode

Code

The slowness occurs on a codebase of around 1000 files. I can’t distill it into a repro, but I can show the type that causes the slowness and an alternate type that does not.

I noticed the slowness when I replaced our implementation of DeepReadonly with the one from ts-essentials. One thing I should note in case it is helpful, is that in our codebase DeepReadonly is only used about 80 times. It’s also used nested in some instances, a DeepReadonly type is included as a property of another DeepReadonly type, for example.

Here is the type from ts-essentials:

export type Primitive = string | number | boolean | bigint | symbol | undefined | null;
/** Like Readonly but recursive */
export type DeepReadonly<T> = T extends Primitive
  ? T
  : T extends Function
  ? T
  : T extends Date
  ? T
  : T extends Map<infer K, infer V>
  ? ReadonlyMap<K, V>
  : T extends Set<infer U>
  ? ReadonlySet<U>
  : T extends {}
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : Readonly<T>;
interface ReadonlySet<ItemType> extends Set<DeepReadonly<ItemType>> {}
interface ReadonlyMap<KeyType, ValueType> extends Map<DeepReadonly<KeyType>, DeepReadonly<ValueType>> {}

Here is ours:

export type Primitive = number | boolean | string | symbol
export type DeepReadonly<T> = T extends ((...args: any[]) => any) | Primitive
  ? T
  : T extends _DeepReadonlyArray<infer U>
  ? _DeepReadonlyArray<U>
  : T extends _DeepReadonlyObject<infer V>
  ? _DeepReadonlyObject<V>
  : T
export interface _DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
export type _DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>
}

Expected behavior:

Both types, when used in our codebase would take a similar amount of time for both a tsc and the initial build of tsc --watch.

Actual behavior:

Our original DeepReadonly takes about 47 seconds to build using tsc. The initial build with tsc --watch also takes a similar amount of time, around 49 seconds.

With the ts-essentials version, a tsc build takes around 48 seconds. The initial build with tsc --watch takes anywhere from 3-5 minutes.

Playground Link:

N/A

Related Issues:

None for sure.

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 4
  • Comments: 51 (21 by maintainers)

Most upvoted comments

These all obscure steps are things that better be done by TypeScript internally. And this is not emit-only issue, but when you have a huge type that being expanded, it will also drastically slowdown intellisense and freeze vscode

FWIW, I tracked down the bad declarations using an emit only build, and then used a file size explorer to determine which files were slowing the build down. This got my build a 3x speedup (70secs -> 20secs)

tsc -p ./tsconfig.base.json --declaration --emitDeclarationOnly --extendedDiagnostics --declarationDir ~/Temp/declarations

I used Disk Inventory X and found big files and cut them down.

Before:

71002092-871aeb80-20d6-11ea-91ea-2544d68e4a61

After: 71001691-b54bfb80-20d5-11ea-9f69-faa5186a614e

However, this doesn’t help in cases where the problematic types are generated dynamically using mapped types, as is the case when using tools such as io-ts or Unionize.

In some cases, you can mask it as well, as i’m doing for mobx-state-tree models https://gist.github.com/Bnaya/5c208956a3e5f3699595f9b9ca0ad3eb#file-good-ts

It’s super verbose but works

@amcasey no problem! Ping me here or on twitter (https://twitter.com/myshov - dm is open) if you need to check your fixes on my codebase.

@mysov I really appreciate the offer - it’s surprisingly hard to get access to repro code. In this particular case, I think we have a pretty good understanding of the problem - we’re just stuck on the best way to fix it. If you want to try it out when we have something, that would be very helpful. Thanks!

I believe I ran into this today, and can reproduce on the playground (or locally). Playground link.

This eventually crashes the watch process locally. Outside watch mode, there doesn’t appear to be any impact. It can be worked around by moving the Filtered<> call out of Filtered and into RemoveEmptyObject.

@weswigham The zip file posted above includes cpu profiles. I think you’re right that it’s spending too much time emitting declarations, probably because it’s not using names. I’m seeing 65% of time in emitDeclarationFileOrBundle and 57% of time spent in typeToTypeNode.

They’re not on GH, are they?

They are, but in a private repo. So unless you have access to private repos or there’s a way to temporarily grant it (i’d need to receive my client’s permission), then no, unfortunately.

Do you have any .tsbuildinfo files that might be affecting incremental build?

Not that I can see. Here’s my tsconfig.json. I have noEmit: true and there’s no built directory generated.

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "moduleResolution": "node",
    "noEmit": true,
    "pretty": true,
    "outDir": "./built",
    "allowJs": true,
    "jsx": "preserve",
    "target": "ESNext",
    "module": "esNext",
    "lib": ["es2016", "dom", "es2017.object", "dom.iterable"],
    "experimentalDecorators": true,
    "noUnusedParameters": true,
    "noUnusedLocals": true,
    "sourceMap": true,
    "strict": true,
    "baseUrl": ".",
    "paths": {
      "*": ["app/*", "types/*"]
    },
    "types": ["googlemaps", "webpack-env", "mixpanel", "gapi", "gapi.auth2"]
  },
  "include": ["./app/**/*"]
}

Here’s the version the profiles were generated with

Version 3.7.0-dev.20191021

And here are some profiles.

  • before.txt is the faster (“ours” above), with --incremental which took 70 seconds i
  • after.txt is the version from ts-essentials with --incremental, which took 7.5 minutes.
  • after-not-incremental.txt is the version from ts-essentials without --incremental, which took only 38 seconds.

Archive.zip after-not-incremental.txt.zip

FWIW, the slowdown appears to come from the recursive application of { readonly [K in keyof T]: DeepReadonly<T[K]> }. If that’s extracted to a separate type like this:

export type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>
}

And used like this:

// ...
  : T extends {}
  ? DeepReadonlyObject<T>
// ...

Then watch is just as fast as a regular tsc compile.

Unfortunately, the editor tools don’t display the type as nicely since they actually show DeepReadonlyObject<{...}> instead of { readonly ... }.