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)
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:
After:
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 ofFiltered
and intoRemoveEmptyObject
.@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 intypeToTypeNode
.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.
Not that I can see. Here’s my
tsconfig.json
. I havenoEmit: true
and there’s nobuilt
directory generated.Here’s the version the profiles were generated with
And here are some profiles.
before.txt
is the faster (“ours” above), with--incremental
which took 70 seconds iafter.txt
is the version fromts-essentials
with--incremental
, which took 7.5 minutes.after-not-incremental.txt
is the version fromts-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:And used like this:
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 ... }
.