Skip to content
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

Slow TypeScript compile time #4323

Closed
brandonkal opened this issue Mar 12, 2020 · 37 comments
Closed

Slow TypeScript compile time #4323

brandonkal opened this issue Mar 12, 2020 · 37 comments

Comments

@brandonkal
Copy link
Contributor

Is work being done to improve the compile time? It takes several seconds on every minor change to recompile one file. This slows down the inner loop. Ideally I'd like to see single-file compile time closer to <=200ms.

@kitsonk
Copy link
Contributor

kitsonk commented Mar 12, 2020

I thought we had an issue, but I can't find it. This is tracked though as one of the key benchmarks of Deno. We have done several changes to improve it, including semi-recently warming the compiler with an old program and including it as part of the snapshot. That increased performance about 30%. Prior to that, we made dependency resolution async, and we did it all upfront before we did the AST parse in the compiler.

It is worth noting that the performance problem isn't linear. The biggest cost is spinning up the compiler all together. Compiling 2 files is only a small amount over compiling 1 file. There are still a few areas of investigation, like looking if buildinfo would increase performance at all. Also, @bartlomieju and I did talk about potentially keeping the compiler running, but that wouldn't help startup.

The next biggest bump, but again, that wouldn't be substantial, is to do the dependency analysis in Rust via swc. Bigger projects it will certainly speed up, but it won't speed up that initial startup time. It has been a while since looking at it in depth, but it will be lots of small wins over a period of time.

On what basis are you suggesting the <=200ms? What sort of machine/system? What sort of "single file"?

@brandonkal
Copy link
Contributor Author

It is good to hear this is being worked on.

According to the graphs, cold relative import is sitting at around 500ms. That's not representative of the slow compile times I am seeing.

I'm currently developing on a 16 cpu 8 GB VM. VSCode eats about 70% of the RAM and the processor is nearly always idle so I don't believe it is for lack of compute power.

By single file I mean I am only making changes to a single file (<=200 LOC) before recompiling. 200ms would mean the experience would be closer to a Go build.

With the 30% improvement number, does that mean it is still doing work on already compiled files?
My two largest files involved are around 20K and 10K lines (kubernetes types) but those never change. There is maybe 60K LOC total. I use Deno for config generation but the compile time is slow enough that I am forced to keep frequently tweaked function parameters in a separate YAML file to avoid the compile overhead.

I would love to have the option to skip type checking and just have the modified file stripped of types especially during development where a warmed-up IDE is providing type checking.

Keeping deno warm would be interesting as booting deno is is probably taking up some wall time as well.

@kitsonk
Copy link
Contributor

kitsonk commented Mar 12, 2020

Whenever you change one file, TypeScript fetches and loads all the dependencies, so it can type check it. The AST is not serializable (see microsoft/TypeScript#33502), so it is difficult to "cache" the parsing of non changed files. There is buildinfo, but I don't know if it would really buy us anything more than the old program does. tsc has a watch mode, and there are incremental builds, but again in most cases this isn't where the time is spent. It is getting the whole thing stood up. The snapshot is pretty huge and even with an old program as part of the snapshot, it we are still spending a lot of cycle times getting warmed up.

I would love to have the option to skip type checking and just have the modified file stripped of types especially during development where a warmed-up IDE is providing type checking.

That is certainly a possibility. That is something we would likely do in swc as well, and it would be blazing fast. Ry and I have talked about it in general terms, but this discussion here is a compelling use case. It certainly would be more preferable than a watch mode.

@ry
Copy link
Member

ry commented Mar 12, 2020

Ultimately I hope we can use SWC instead of the official TS compiler - this will offer massive exe size and perf improvements. @kdy1 has a very interesting WIP branch here https://github.com/kdy1/swc/tree/ts-checker but it's obvious we won't be able to replace TSC with SWC any time soon.

For the foreseeable future, there may be some yet undiscovered bugs in our interface to TSC that could help this, but it seems unlikely to me that we'll see any order-of-magnitude gains.

TS has said they're working on performance in the next release - we'll see what that amounts to.

One thing we could do is offer a switch to not do type checking on every run - or even asynchronously. This would speed things up greatly... but it seems like a hack to me.

(I'm also surprised we don't have an issue for this already. As @kitsonk mention, we've been tracking compile speed since day one on the benchmarks page.)

@kitsonk
Copy link
Contributor

kitsonk commented Mar 12, 2020

One thing we could do is offer a switch to not do type checking on every run

I do think though that a --no-check which would do a transpile only could be a big boost. As @brandonkal says, if you are using your editor to enforce your types, and you just want to quickly run a workload that you have validated somehow, but isn't fully in your cache yet, why do all the type checking in Deno? Why not just have it do the transpile. Personally I think we should do it and it does take "pressure" off performance in other areas. It feels like a legitimate workflow to me.

@brandonkal
Copy link
Contributor Author

Agreed. A --no-check option coupled with having the editor compile on save (i.e. deno fetch --no-check modified-file.ts) would largely eliminate the friction and perceived slowdown of developing Deno projects. Then the incremental TS compile time improvements would largely be a bonus.

@dsherret
Copy link
Member

I'm probably bringing up an already discussed point, but what are the current thoughts on making type checking opt-in rather than the default?

If it's not the default, then transpiling could always be done with swc. As has been said, during development people's editors can do the type checking and perhaps there could be an additional --type-check flag that will fetch the TS compiler on first run then have a type checking phase that gets run in parallel to transpiling (that would be useful for the CI).

@ry
Copy link
Member

ry commented Mar 13, 2020

I'm open to having the default not type check. Maybe rather than having --type-check we could just move it to deno lint?

@kdy1
Copy link

kdy1 commented Mar 13, 2020

If required, I will extract typescript stripper from the transforms crate. It would be easy as it does not depend on other transforms.

@kitsonk
Copy link
Contributor

kitsonk commented Mar 14, 2020

Hrmmmm... that makes me totally uncomfortable to be honest... One of the main use cases for me has always been the support for TypeScript which enforces the TypeScript syntax. Not type checking defeats the whole purpose of TypeScript to me. That is like saying "just run the JavaScript without syntax checking". Yes, there are other possible ways to enforce the types before running the workload in Deno, but those are optional.

I would personally be very very very sad if we didn't type check by default. Type checking is a heck of a lot more than linting.

@brandonkal
Copy link
Contributor Author

I would find it weird for type checking to be a part of deno lint. In my experience even outside of Deno, TypeScript checking has always been much slower than running ESLint, so it is preferable to have them as separate command or flag.

I'm open for it to not type check by default especially if a lock file is used. With compiled languages we can just distribute a binary and all user's don't need to run a checker.

Given this:

  1. The distributed script url is type checked. CI status and all.
  2. It has a lock file so it will fail if files are not the same.

Then type checking isn't really necessary there.

  • It's nice but it slows down the first run for users significantly (who may not even know what TypeScript is)
  • It is a waste of compute resources.
  • It also slows down all the Docker builds which will run deno fetch to precompile resources. I've already type checked the program in an earlier CI stage. No need to do it again.
  • It will slow down the time to attach a runtime debugger

Looking at the number of downloads for @babel/plugin-transform-typescript and fork-ts-checker-webpack-plugin vs ts-loader people prefer having checking and type-stripping be separate processes by a large margin (16.6M+16.6M vs 5M). Granted download numbers are not a great way of comparing preference, but it is apparant that many developers would rather have type checking run as a separate process from transpile. You don't have to wait as long to see a result in the browser but type-checking can still continue. So that is one reason to make --no-check instead a default.

As @dsherret mentions, that would enable using SWC for transpilation which would help speed up the developer loop.

Still I would find it a bit odd if the default was off. I would suggest --no-check or maybe a --fast-compile flag plus a DENO_SKIP_CHECK environment variable. The environment variable means users who have an IDE (which means 99% of the developers) can disable redundant type checking easily but unset it to see if the editor and Deno TypeScript checkers disagree.

@kitsonk
Copy link
Contributor

kitsonk commented Mar 14, 2020

I wouldn't like --fast-compile as it seems to indicate it is a fast mode, so why not do that all the time... the flag should indicate what is actually changing, not what the potential benefit is.

@ry
Copy link
Member

ry commented Mar 15, 2020

In any case I think --no-type-check (or whatever we call it) would be the least disruptive and easiest way to introduce this feature. After that has shipped we can have another discussion.

@Ciantic
Copy link

Ciantic commented Mar 20, 2020

Besides speed, having --no-type-check option would allow to run badly typed code, which could be a bonus for some cases. Of course as a default it would not be a good thing, because it's basically same as running JS.

P.S. Type-check speed can probably be improved a lot in the future with all crazy caching mechanisms especially at development time.

@tony-go
Copy link

tony-go commented May 14, 2020

Hi Guys 👋

I read this at https://deno.land/v1

We certainly think there are improvements that can be done here on top of the existing TypeScript compiler, but it's clear to us that ultimately the type checking needs to be implemented in Rust.

TSC must be ported to Rust. If you're interested in collaborating on this problem, please get in touch.

I'm very curious about that. Here the good place to follow the discussion?

@dsherret
Copy link
Member

dsherret commented May 14, 2020

@tony-go I think follow it here swc-project/swc#571 and in this thread.

@tony-go
Copy link

tony-go commented May 14, 2020

Thanks @dsherret

@dsherret
Copy link
Member

dsherret commented May 15, 2020

By the way, on that idea of rewriting the type checker in Rust, I don't believe it would be worth it to do... the TS team has a lot of talented individuals working full time professionally on the problem. It will be a massive undertaking to catch up and use up a lot of time staying up to date. I think we could all use our time more effectively solving other problems. I think something like "solution 2" mentioned in #5431 would be a lot better because we could remove the type checker from the runtime.

Regarding the concern of "defeating the whole purpose of TypeScript": in my opinion, the largest value Deno brings to me is that it can run my TS code without me needing to setup or do an intermediate step. I don't think it defeats the whole purpose of TypeScript at all because I can still run type checking on that code... I'm just doing that separately from the runtime.

@calvintwr
Copy link

Just like to express my two cents worth. It may not be somewhat relevant to this issue but, well, I see so many people raising compiling and performance issues with TypeScript that I finally feel compelled to debunk the TypeScript bandwagon. TLDR; Even some parts of Deno are written in vanilla JS. Your solution is to not write all your code in TypeScript, but to exercise judgement and use as required. For JS codes, use type-checking library for runtime checking, and you won't need to complain about TypeScript's "lack of" for things it is not meant do.

Sure, TypeScript is one certain way to enforce some discipline and strengthen typing. But so much code ends up using variable: Any everywhere, that it consequently defeated the original purpose.

Where one is compelled to repeatedly use Any, it's usually an indication that this area is best written in JS. Or one may say it's just bad code, or that Any ought to be omitted in the first place (I fully think so, and I will explain further below). But the fact that Any is included for TS, reflected the irrefutable need for flexibility, which derives far more value in giving good programmers the ability to write clean code fast and not be burdened by typing and compiling, as compared to omitting it to whip bad programmers in line.

In the end, if you think writing in full Typescript is a silver bullet, you gave up the very good perks that Javascript offered, and exchanged them for an almost strong-typed language; that if you had wanted a strong-typed language, those perks would have to be given up for anyway. If so, you are better off using other languages.

This is not even mentioning the lack of runtime type-checking, and how confused TS backers have believed TS to be the be-all and end-all, existentially disrupting APIs that need validation at runtime. See how clumsy Typescript becomes to do runtime checking.

TypeScript and Javascript should possibly be best used in combination. And Javascript should just widely use a small type-checking library that will catch runtime violations (linting is a bonus). It can easily be very minimal, like just is('object', argument). I know this because I ran into the same problems, insofar that I wrote a module to address it.

Further, even without strong-typing, quality of an application are nowadays assured by tests, an established basic requirement, that would work in tandem to compliment the type-checking within the code.

This limits the use of TypeScript to just a small area of core features that are heavily depended upon. Divide your code up into dire areas (use TS) and others that will benefit from flexibility (us JS). Both areas can use a JS type-checking library. Albeit one can say its up to discipline to write tests or use the library, such is true for using Any, anyways.

So take away Any, or not use it at all. We should absolutely still continue to have Typescript. And then we will end up with an app that is partially in TypeScript and Javascript. One would need to make the judgement on which part of the app to use which. We should not be dug in on either of trenches -- TypeScript or Javascript, but that both are one and the same, to be used in combination.

@brandonkal
Copy link
Contributor Author

@calvintwr This issue isn't about discussing the validity of using TypeScript. It's about making compile time faster or skipping it (in favor of the faster in-IDE type checking). This is particularly important for deno because all users (including end-users and serverless containers) need to wait for that slow type-checking. This is why I agree with @ry that making no checking be the default is worth considering. Developers can easily enable type-checking in Deno or more likely, let their IDE or CI do that.

That said, if I am able to or already have a compiling step (babel etc), I'm going to use TypeScript. For me, it is more of a question of using noImplicitAny or not. I prefer noImplicitAny: false in my projects. It's the ancillary features that make TypeScript more productive in my workflow: better IDE auto-completions, IDE-assisted refactoring, and code navigation. If something is implicit, I'm fine with that during development. Tests catch more bugs than TS ever will. I prefer faster iteration over complex types that give the illusion of type safety. Without OCD-level checks enabled, I'm not compelled to mix plain JavaScript in my TS codebases.

@calvintwr
Copy link

@brandonkal I see. Yup I missed out the point on IDE type checking, that makes sense. Thanks for highlighting.

To further this quasi-conclusion on --no-type-check, which kind of is a devil-or-the-deep-blue-sea option, is it possible to consider something like --type-check-only [dirs/files]? Intent is to turn off type-checking for all files and dirs except those that are specified -- which depending on the use case, would most likely be the new code. And if one so wishes to, it can be used to turn off all type-checking, amounting to --no-type-check.

  1. This preserves type-checking by default.
  2. This gives flexibility to check only new code and skips old and proven codes
  3. It is less of a devil-or-the-deep-blue-sea option.

@ry @kitsonk

@brandonkal
Copy link
Contributor Author

That increases the complexity a lot with little real benefit.

@apiel
Copy link

apiel commented Jun 8, 2020

As mention by @brandonkal lot of people tend to do type checking in a separate process. So, giving the possibility to deactivate type check with --no-type-check would be a good start. Also switching to another thranspiler than TSC sound super cool but then we will not be up to date to the latest version of TS, till all update as been done in the alternative transpiler. Therefor sticking to the official transpiler might be better and might sound more reliable to developers, but you could give the option to the Deno users to use another one like swc or babel. I think to have this switch build in run-time would be optimal, so we could switch dynamically of transpiler Deno.setTranspiler().

@David-Else
Copy link

I think using anything other than the official TypeScript compiler would be crazy. If you need speed for development, then just do the type stripping with --no-type-check, perfect!

@Ciantic
Copy link

Ciantic commented Jun 8, 2020

Hmm, I thought that switching transpiler should be the easier task. SWC, esbuild etc. uses faster transpiler, because they just remove the types and calls it a day.

Switching a type checker to some self-made thing is a crazy task which is not doable in my opinion. The type checker is TS teams task.

I also love that all the discussions here comes back to the same square, should the type checking be on by default or not? I think for TS the type checking should be turned on, but it could be ran asynchronously in separate thread while transpiler does it's thing as fast as possible. We would get the fast restart and type-safety.

@apiel
Copy link

apiel commented Jun 8, 2020

I think for TS the type checking should be turned on, but it could be ran asynchronously in separate thread while transpiler does it's thing as fast as possibl

@Ciantic but why to try to build a custom way to handle types. If you want to skip types and you use --no-type-check. If you use type checking, we want to be sure that the type are valid before to run the application. This is important when you have some automated deployment infrastructure.

@Ciantic
Copy link

Ciantic commented Jun 8, 2020

@apiel there is ideological difference here. I don't want to rehash it here. (Edit I don't know what your opinion is here, I'm just guessing).

Basically there is three type of people here. Some who want strictest type checking TS can offer, some who wants some type checking (don't like all the strict flags), and some who don't want any type-checking. It became pretty clear with this issue #3324.

It's fine if you belong to the camp "deno needs not to do any type-checking", I don't take issue with it, it's your view. I just happen to belong to the camp who believes if Deno takes TS in, it should have strictest type checking possible.

If we go without type checking TS at all by default, in my opinion, then Deno should not take TS as input files. It should take only JS files in (and strongly encourage for generated d.ts files for IDE experience).

I do support however having an --no-type-check option, but not enforcing it as a default.

@apiel
Copy link

apiel commented Jun 8, 2020

@Ciantic actually I am more on the side of having type checking by default like tsc is doing.
It just often happen to me, to deactivate type check manually because it is too slow. But I will always try to type check before to publish my code. I need both and I want to be able to decide. Also, in my opionion, sticking to the default behavior of TSC would be the less confusing, they have type-check by default, therefor Deno should have it as well by default.

After concerning swc or some babel like tools, this should be up to the people using Deno. By default, we use standard (alias TSC) but deno would give the possibility to easily switch to another transpiler, either with some flag in the cli deno run --transpiler=customTS.js --allow-... mod.ts or maybe even during run-time Deno.setTranspiler(...) but this would be more challenging.

@wongjiahau
Copy link
Contributor

wongjiahau commented Jun 9, 2020

I believe this can be partially solved by not advocating the mod.ts and deps.ts convention. Referring #6194

@nayeemrmn
Copy link
Collaborator

@wongjiahau No, you can see this issue as being about about slow TS compile time for some given dependency graph. Suggesting ways to reduce dependencies is entirely orthogonal to that.

@wongjiahau
Copy link
Contributor

@nayeemrmn check out this repository where I demonstrated how the mod.ts and deps.ts convention actually slows down compilation time by 200%.

@kitsonk
Copy link
Contributor

kitsonk commented Jun 9, 2020

@wongjiahau if there is a significant difference, we need to investigate that, as there is potentially an inefficiency. Going around posting in every channel you can find to not do a convention that is an effective convention because of an observed performance difference isn't helpful. Logically there should be little to no difference, so the fact that there is potentially one is potentially a bug, not a change to conventions.

@r-tae
Copy link
Contributor

r-tae commented Jun 9, 2020

@wongjiahau Your example doesn't have equivalent setups for the two things you're benchmarking, you're actually testing how well Deno can optimise out unused module imports.

If you want a fair comparison then you need both x.ts and y.ts to import the same number of modules. I have the difference at around 10% when I import all three modules (a, b and c) in both.

@wongjiahau
Copy link
Contributor

wongjiahau commented Jun 9, 2020 via email

@wongjiahau
Copy link
Contributor

The latest Typescript 3.9 might be able to partially solve this issue. Reference https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#speed-improvements

@kitsonk
Copy link
Contributor

kitsonk commented Jun 18, 2020

Deno has 3.9.2 since v1.0 which includes these speed improvements.

@bartlomieju
Copy link
Member

We've got support for incremental compilation now, which mostly alleviates this problem. For further information see #5432

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests