ts-loader: tsloader's project reference support seems to broken
Expected Behaviour
Project references compile/transpile way too slow, this can be tested and reproduced, I put extensive benchmarking below. transpileOnly: true
doesn’t make any difference on ref compile times so there must be something wrong, more details:
- tsloader compiles refs slower than tsc- -b -w (no news, but just as a reference for the next points!)
- tsloader+transpileOnly transpiles refs exactly as slow as tsloader omitting transpileOnly (compare BROKEN 1 with BASE)
- compare BROKEN 2 also to the babel+tsc-noEmit version (OPTION 3) which shares the same architecture and refs are transpiled ultra fast
- tsloader seems to affect also ts-fork-checker perf negatively: https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/issues/463#issuecomment-681318677
tldr ATM it’s recommended to go for babel-loader
for fast project reference transpilation and a dedicated tsc -b -w --noEmit
process for type checking until tsloader’s bug is fixed
CC @johnnyreilly, @sheetalkamat, @piotr-oles, @appzuka
thanks to r/keiser_sozze for hinting me to babel+tsc noEmit.
Here again the updated comparison table, pls focus on OPTION 3 and BROKEN 1 and 2 and there, the bold figures highlight the broken transpilations:
setup | type-checking? | initial build w/o changes before (warm start) | initial build w/ changes before | initial build after rm -r dist (cold start) |
rebuild on 1st change in non-reference | rebuild on 1st change in reference | rebuild on 2nd change in non-reference | rebuild on 2nd change in reference |
---|---|---|---|---|---|---|---|---|
OPTION 1 tsc -b -w + webpack-dev-server just bundling tsc ’s output, aggregateTimeout: 0 |
✔ | 1.5s | 13s | 14.7s[2] | 2.3s | 3.4s | 2.2s | 2.4s |
webpack-dev-server+tsloader, fork-ts-checker | ✔ | 5.4s | 5.2s | 9.6s | 1.8s | 4.5s | 1.1s | 3.3s |
BASE webpack-dev-server+tsloader | ✔ | - | - | 9.5s | 2.1s | 4.2s | 1.9s | 4.4s |
BROKEN 1 webpack-dev-server+tsloader, transpileOnly | - | - | 8s | 1s | 4.4s | 0.4s | 3.7s | |
BROKEN 2 webpack-dev-server+tsloader, transpileOnly, tsc noEmit | ✔ | 5.2s | 5s | 12.2s | 1s | 6s | 0.9s | 6s |
OPTION 3 benchmark winner, webpack-dev-server+babel, tsc noEmit | ✔ | 6.7s | 6.8s | 6.6s | 1.7s | 1s | 0.3s | 1s |
BROKEN 3 webpack-dev-server+tsloader, transpileOnly, fork-ts-checker | ✔ | 4.7s | 4.5s | 9.9s | 1.2s | 5.5s | 0.9s | 6s |
just tsc -b -w without bundling |
✔ | 6.6s | 11.5s | 13.2 | 1.9s | 2.9s | 1.67s | 1.94s |
just webpack-dev-server bundling tsc ’s output, aggregateTimeout: 750 |
- | - | 1.56s | 0.5s | 0.6s | 0.4s | 0.3s | |
just webpack-dev-server bundling tsc ’s output, aggregateTimeout: 0 |
- | - | 1.5s | 0.4s | 0.5s | 0.5s | 0.4s | |
webpack-dev-server+tsloader w/ happyPackMode, fork-ts-checker w/semantic+syntatic, threadloader w/infinite pool | ✔ | -[1] | - | - | - | - | - | - |
webpack-dev-server+ts-loader, tO true, import dists | yes | 3.7s | 4s | 9.2s | 1.3s | 6.8s | 0.7s | 5.3s |
tsc -b -w w/o bundling, import dists | yes | 6.3s | 6.61 | 6.6s | 1s | 3.6s | 0.9s | 2.26s |
webpack+ts-loader, tO true, fork-ts, import dists | yes | 3.4s | 3.4s | 8.8s | 3.3s | 8.9s | 3.4s | 8.6s |
tsc -b w/o bundling, import dists | yes | 6.5s | 6.79s | 14.4s | 6.6s | 4.29 | 6.96s | 6.16s |
Actual Behaviour
BROKEN 1 should be much faster than BASE
Steps to Reproduce the Problem
git clone https://github.com/RyanCavanaugh/project-references-demo, take your preferred webpack config and play around with the settings in the benchmark table.
Location of a Minimal Repository that Demonstrates the Issue.
https://github.com/RyanCavanaugh/project-references-demo + settings from the table
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Comments: 18 (8 by maintainers)
@desmap this is open source. No-one is paying me to build
ts-loader
, I do it in my spare time with the help of all the generous people who contribute to the project, and have contributed in the years it’s been running. This is a generousity game we’re all playing.The lib is not perfect but it’s very useful to many people. You are not entitled to anything, but you are welcome to join in.
Perhaps you could help make
ts-loader
even better and I invite you to do so ❤️🌻@desmap, thanks for sharing the repo.
I can see that you are not actually using project references in your front end build. In your backend build you execute
tsc -b
, which builds the entire project. After this you see that thedist
folder gets populated with all the transpiled code.For the front end, when you run
yarn build-another-package
you are running 2 commands:The first builds the project with webpack and babel-loader. Babel-loader just strips out the types, it does not check for syntax errors (there are other aspects of Typescript it does not handle either, such as const enums). For this reason, you run the second command, which should report Typescript syntax errors.
Both of these commands run, and a bundle is produced in dist, but neither use project references and the project references are not compiled. Run
yarn build-another-package
after runningyarn clean
and you will see that only the final bundle is indist
, not the compiled references.So to be clear if anyone comes across this thread, babel-loader does not understand project references. You will notice that babel-loader produces a warning
"export ‘Dog’ was not found in ‘./dog’
. This is because Dog is an exported Typescript interface. Babel-loader simply strips this out because it does not understand it and then reports that it is missing. If you use ts-loader you will not get this warning.Webpack with babel-loader does build the project, even if you have not compiled the references. This is because you are building from the typescript source files, not the compiled references. In your
another-package/index.ts
you import from the zoo typescript source:If you are using project references and you want to benefit from using the precompiled projects you should be importing from the compiled javascript:
This gives you the benefit of using the pre-compiled files when building your project, which will reduce the warm-start time. It does mean that if you make a change to a reference you will need to re-build the reference. You would need to be running
tsc -b -w
in a separate shell and then you would need to add the 2-3 seconds it takes tsc to build the reference to the 1s it takes webpack to bundle the project.In your benchmarks, you see essentially the same time to rebuild after a change in references and non-references for your Option3. This is because Option3 does not use references so it is doing the same thing. The settings with ts-loader (your Broken2) takes the same time for non-references but much longer for changes to a reference. This is because you are asking ts-loader to do everything that babel-loader does and then build the references, even though they are not used. If you set ts-loader to run with
transpileOnly: true
and withprojectReferences: false
I expect it would perform the same as babel-loader with better handling of typescript.I don’t believe it is fair to say that there were some config/structural errors in my repo. We have made different choices and those choices make different performance tradeoffs. If rebuild time is most critical to you then perhaps your configuration is best for you. The babel-loader vs ts-loader is not the relevant thing to look at here - they both have fast rebuild times. The issue here is comparing webpack in watch mode vs
tsc -b -w
. Your setup uses project references for backend build but not for frontend. They share a single typescript codebase so I don’t think there is a problem with that. You gain fast development rebuilds at the expense of a slower development start time. If you are happy with that tradeoff that is fine. Others may have different priorities and choose to go a different way.I did bloat the codebase as you suggested and found ts-loader is behaving exactly as expected. Its performance with project references is the same as running
tsc -b -w
in a separate process. Having project references integrated into ts-loader means you can build your project with a single webpack command rather than managing a 2-stage flow.Your benchmarks have uncovered some interesting points about different ways to configure projects. I don’t believe they show that there is a bug in ts-loader or that ts-loader’s performance is worse than alternative methods.
It is also clear that using project references with webpack is not straightforward and documentation is badly needed. I intend to add some documentation and examples to ts-loader so that others can get the most of out this great feature which has been added to tsc.
@desmap, I forked the demo repo as you suggested and added webpack.config.js to test the various options. In the readme I have detailed what I found. I won’t repeat that all here but in summary, ts-loader, project references, transpileOnly & ForkTsCheckerWebpackPlugin are all working as expected. I am not seeing a bug in ts-loader here. You can see the repo at:
https://github.com/appzuka/project-references-demo
It is certainly possible that with larger projects we will see a significant performance slowdown. I will try to expand the repo to test this although I have another project using all of ts-loader, project references, transpileOnly & ForkTsCheckerWebpackPlugin and it works fine (initial build is around 30 seconds).
Hopefully, you can see where this code differs from yours and understand why you are seeing something different. If you can share a repo showing the behaviour you benchmarked I’ll take a look.
Are you serious?
I don’t know the code base, I spent now 48h in benchmarking your lib and asking me for a PR is—no offense—borderline.
I could offer you to make a PR on the docs which states ‘project ref support is highly experimental’, so other users don’t fall in the same rabbit hole like I did. It’s your lib and you hurt your and
fork-ts-checker
’s users with keeping this bug alive. It’s your call and I wish you all the best!I submitted a PR to for ts-loader’s docs to make it clear that
transpileOnly
has no effect on project references.The code I used to confirm ts-loader is running correctly on a large codebase is available in the
benchmark
branch of the repo I shared:https://github.com/appzuka/project-references-demo/tree/benchmark
The steps to run the benchmarks and the times I obtained on my system (WSL2, i7 3.4GHz) are in the readme.