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:

  1. tsloader compiles refs slower than tsc- -b -w (no news, but just as a reference for the next points!)
  2. tsloader+transpileOnly transpiles refs exactly as slow as tsloader omitting transpileOnly (compare BROKEN 1 with BASE)
  3. compare BROKEN 2 also to the babel+tsc-noEmit version (OPTION 3) which shares the same architecture and refs are transpiled ultra fast
  4. 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)

Most upvoted comments

@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 the dist folder gets populated with all the transpiled code.

For the front end, when you run yarn build-another-package you are running 2 commands:

webpack
tsc --incremental --declaration --emitDeclarationOnly --preserveWatchOutput

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 running yarn clean and you will see that only the final bundle is in dist, 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:

import { createZoo } from '../zoo/zoo'  // imports from zoo.ts

If you are using project references and you want to benefit from using the precompiled projects you should be importing from the compiled javascript:

import { createZoo } from '../dist/zoo/zoo' // imports from zoo.js

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 with projectReferences: 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.

If you’d like to look into submitting a PR to resolve this we’ll happily 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.