ts-node: Compilation is unbelievably slow
Hi, for some reason we have performance problems when using ts-node in our mocha tests. It takes about 500 ms to compile an empty ts file. I added some console.logs to measure the time to run this very line: const output = service.getEmitOutput(fileName)
and this line takes 500 ms to run even though the content of the file is an empty string (the code
variable on the line 314 is empty string). Actually, all our tests files take too long to process, while the production files take a few milliseconds to compile. So any *.spec.ts
takes about 500 ms to compile, a regular *.ts
file takes about 20 ms. Do you have any idea what could be the root cause or how should I debug it more?
We use latest mocha, ts-node and typescript, but we tried some old versions too and the problem persists. The tsconfig.json
:
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "dist",
"rootDir": ".",
"declaration": true,
"target": "es2017",
"lib": ["es2017"],
"module": "commonjs",
"moduleResolution": "node"
},
"exclude": [
"node_modules",
"dist"
]
}
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 140
- Comments: 168 (32 by maintainers)
Links to this issue
Commits related to this issue
- workaround: ts-node is super slow, add cli options to speed up see TypeStrong/ts-node#754 — committed to kiwikern/shassi-nest by kiwikern 5 years ago
- Update multiple dependencies to latest ts-node omitted due to https://github.com/TypeStrong/ts-node/issues/754 — committed to katanacrimson/ByteAccordion by katanacrimson 5 years ago
- Update multiple dependencies to latest ts-node omitted due to https://github.com/TypeStrong/ts-node/issues/754 — committed to katanacrimson/SBON by katanacrimson 5 years ago
- Update multiple dependencies to latest ts-node omitted due to https://github.com/TypeStrong/ts-node/issues/754 — committed to katanacrimson/SBAsset6 by katanacrimson 5 years ago
- Update dependencies: mocha (security), ts-node (perf) Security advisory from GitHub for minimist, a dependency of Mocha: https://github.com/laughinghan/mechanical/network/alert/package-lock.json/mi... — committed to laughinghan/mechanical by laughinghan 4 years ago
If anyone can give me temporary access to a slow repo, let me know! I can quickly take a look into the issue and happy to sign any NDA, etc you need. If not, can I get a sense of peoples
tsconfig.json
that’s slow - are you relying onfiles
,include
,exclude
,rootDir
or other to compile with TypeScript?Let’s try with reactions:
files
include
exclude
rootDir
Have you tried
--transpile-only
mode?I’ve run into similar issue today. After ts-node upgrade from 7.0.1 to 8.0.1 my mocha test suite became very slow (32s vs 4s). Adding TS_NODE_TRANSPILE_ONLY=true resolved my issue. Thanks @cspotcode
That said, an ode to you @blakeembrey and the rest of the ts-node team. Your work on this free, open-source tool and your visibility/availability to its users is much appreciated.
We had to downgrade too. This is what happens in our project 💥
7.0.1
8.0.3
Reproducing the issue
I also created a project to reproduce the issue: https://github.com/lukaselmer/reproduce-slow-ts-node
7.0.1
8.0.3
With TS_NODE_FILES=true
With TS_NODE_TRANSPILE_ONLY=true
With TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true
It seems that even with the additional options,
8.0.3
is still 2-7 times slower than7.0.1
.I think I had a breakthrough. Compilation gets way faster when we implement
getProjectVersion
in ourLanguageServiceHost
. I think without that method implemented, the compiler has no choice but to routinely throw away a bunch of internal state. For example, for every required ts file, we make 2 calls back-to-back: one to get output JS, and the next to get diagnostics. Without implementinggetProjectVersion
, the compiler has to redo a bunch of work internally for both of those calls, which is crazy because nothing has changed.For example, if you run the following assertion:
… it fails in ts-node today, but it passes when I add an implementation of
getProjectVersion()
.TS source explaining
getProjectVersion()
EDIT: Here’s where this happens inside the compiler: https://github.com/microsoft/TypeScript/blob/master/src/services/services.ts#L1201-L1394 When project version is not the same or not implemented, a new
Program
andTypeChecker
must be built. Implemented and explained here: https://github.com/Microsoft/TypeScript/pull/3131/#issuecomment-103121030Even when we don’t provide a project version, the language service internally checks if the program is up-to-date using a more CPU-intense check, looping through all files, config values, etc, to see if anything changed. This check is failing, so there must be something else we’re doing wrong. https://github.com/microsoft/TypeScript/blob/master/src/compiler/program.ts#L555-L650
EDIT2: Found where it’s failing. When any source files are omitted from
getScriptFileNames
the check fails because the internalHostCache
doesn’t have those files. Caused by the revert made between v8.4.0 and v8.4.1 https://github.com/TypeStrong/ts-node/issues/754#issuecomment-536097118 Maybe related: https://github.com/TypeStrong/ts-loader/issues/949I filed a bug on Typescript to see if the
Program
invalidation is a bug on their end. https://github.com/microsoft/TypeScript/issues/36748Released as 8.8.0, please let me know if anyone sees a regression in other parts of the application (or errors with TypeScript as happened in 8.4.0).
--transpile-only
does speed things up considerably for me. I don’t want to lose the type checking though!@brunolm Your update on 8.4 inspired me to revisit the change in that version. Can you try https://github.com/TypeStrong/ts-node/pull/985 at all?
For now, please try using the environment flag from https://github.com/TypeStrong/ts-node#cli-and-programmatic-options -
TS_NODE_FILES=true
. I’ll attempt refactoring to the newer TypeScript watch API and see if it improves performance in the coming weeks.In my case with
.mocharc.js
, replace:require: ["ts-node/register", ...]
with:require: ["ts-node/register/transpile-only", ...]
resolved my issue.It doesn’t seem fixed with version
8.1.0
😞@blakeembrey
✅ https://github.com/TypeStrong/ts-node/pull/985 (
npm i -S TypeStrong/ts-node#be/improve-perf-again
)Looks like you nailed it!
❤ 😄 🎉
#963 implements
getProjectVersion
, which should workaround microsoft/TypeScript#36748, meaning the compiler needs to typecheck 3x less than before. This will only affect you if you have not enabledtranspileOnly
/--transpile-only
/TS_NODE_TRANSPILE_ONLY
norcompilerHost
/--compiler-host
/TS_NODE_COMPILER_HOST
.If you’re really eager or curious, you can test it by putting this in your package.json deps:
"ts-node": "https://github.com/TypeStrong/ts-node.git#ab/add-getprojectversion",
Setting
TS_NODE_DEBUG=true
will emit extra debug statements related to when and how often the compiler needs to rebuild the typechecker.That’s great feedback, thanks! If I can gather another piece of positive feedback in terms of performance on a large project, I’ll look into releasing this as v9 along with other breaking changes.
@kfrajtak I’ve tried esbuild and it is unbelievably fast. I moved to it. Compile + run is much faster than ts-node. Thanks.
Edit: I’m using
esbuild --watch
andnodemon build/out.js
nowEdit 2: I’ve tried to use other project using decorator, but its Interpolation is still different from original
tsc
. so I’m stickingtsc --watch
in that project.I have read over all the comments, the issue is still open and it’s been close to 9 months. The latest version still has this problem and the only solution I can see here is to role back to 7.0.1
Are there any updates on this issue and when this will be resolved.
Using
--files
helps run the tests a bit faster for my colleague but for myself they do not complete even after 11 minutes whilst I was making tea.I use
node-demon
withts-node
for automatic restart of the server on code edit. I still see a consistent 10x slow down for restart, between versionsv7.0.1
andv8.10.1
ofts-node
. I am using Linux Mint.I will try and create a minimal reproducible example, but I just wanted to note that the speed regression is still very clear, at least on my machine.
❌
ts-node@8.7.0
❌
ts-node@8.6.2
✅
ts-node@7.0.1
@blakeembrey I’ve made a minimal repro at https://github.com/davidgruar/tsnode-perf-repro - please check it out and have a play. Loading just three test files with fairly few dependencies takes more than a second, and the more imports you add, the longer it takes.
Closing because some solutions are already available (
--transpiler
), other aspects are tracked by other open issues (incremental builds), and unfortunately this thread has not yielded good reproductions with which we can test.--transpiler
withswc
yields the fastest compilation when skipping typechecking, beating out even a fully-cached solutionCan you use the power of esbuild? https://esbuild.github.io/
@Alexandredc instead of having your nodemon config watch your
.ts
files and executets-node
on the entryts
file on changes, You can instead runtsc --watch
(optionally with"incremental": true
in yourtsconfig.json
) when the.ts
files change to generate thejs
files in the"outputDir"
folder, then runnode / nodemon
against the generated entry.js
file@christianrondeau It’s currently on
master
but cannot support a use-case that used to be supported. I only discovered this after trying to test another bug in the backlog, so I haven’t released it yet. Most likely I’ll be looking to put it behind a flag as part of v8 for a bit.@blakeembrey I’ve tried the incremental program branch and it launches our app in 10 or 11 seconds, whereas 8.4.0 takes around 16 seconds
I’ve been using 8.3.0 for a while. Tried upgrading to 8.4.1 today and startup time went from ~16s to ~75s.
also had to downgrade
8.0.3
to7.0.1
to fix the performance issue, time to start tests was like 3 minutes vs few seconds difference,TS_NODE_FILES=true
didn’t help anythingI’m just using
tsc --noEmit && TS_NODE_TRANSPILE_ONLY=true ts-node index.ts
in my project, and it’s 3x faster thants-node index.ts
( there are 500+ ts files in my project. before: 96 seconds, after: 32 seconds ).See also: #1160 which allows opting into the swc compiler via ts-node.
I’ve met same issue on developing web app/API server using express + ts-node-dev(ts-node). So, I’ve created express-lazy-router for reducing this issue. This is inspired by webpack’s lazyCompilation.
express-lazy-router help us to reduce the compilation time at start time by compiled when needed. 📝 It is for webapp using express, it is not for testing
@khaledosman @Alexandredc
My current solution is tsc + nodemon, but I’m still doing the configuration for Ava. The downside is the complex configuration.
ts-node
andtsconfig-paths
is a simple configuration, works nicely forfs.readFile
and ts path mappings and Ava. But tsc + nodemon is 2x speeds up my project.The basics are:
And the last thing is to add deps:!
edited: You may also needs
source-map-support
I just tried 9, and altough it feels somewhat faster on the initial compilation, it recompiles at almost the same speed as the initial on file changes, which overall makes it much less useable than v7 (need to wait 4 mins to recompile after changing a single character in code). I switched back to v7.
Whether or not the original slowness issue has been solved, I’m happy to leave this ticket open a bit longer, because the discussion is still useful. Ultimately the goal with these tickets is to help us make ts-node better, and it’s still serving that purpose for me.
Where individual issues are being identified, we’re tracking and fixing them as separate tickets. Additionally, the recent change in 8.8.0 may need to be reverted since it triggers the regression test from #884 So it’s possible we’re not out of the woods yet.
See also: #996, which links to the other relevant tickets.
Also, specifying whether or not you’re on Windows is very helpful! Since it seems we may have a windows-only performance problem related to
/
vs\
paths.Big kudos to you @blakeembrey for sticking with this problem this far! I have not tried out 8.8.0 yet, but the reactions that people have made about it so far assure me that you have indeed solved the problem.
Ẹ ṣeun pupọ! Thank you very much!
Please try
ts-node
v8.7.0. It’s been hanging out on thenext
dist-tag but I just promoted it tolatest
.https://github.com/TypeStrong/ts-node/releases/tag/v8.7.0
The
LanguageServiceHost::getProjectVersion
implementation should help some projects quite a bit.I created a very minimal reproduction here: https://github.com/TypeStrong/ts-node-repros/tree/754 It looks like require-ing each file with typechecking turned on eats up about 60ms, even if the file is basically empty.
Possibly related: discussion here about how getting diagnostics per file is apparently slow no matter what: https://github.com/typescript-eslint/typescript-eslint/issues/1335 But I don’t know if I understand the details well enough.
Passersby interested in implementing that, here’s the announcement linking to PR & docs: TS 3.6 APIs to support
--build
and--incremental
.@gruckion If you want it to be improved, you’ll need to submit a PR. Based on this discussion I’m unclear of how to improve the performance - the only possible configs are
--files
or--transpile-only
. The only major gripe I can understand from comments is that people liked the file system cache a lot, which could be re-enabled but not as default because it causes subtle hard-to-debug issue. Using the new incremental compilation by TypeScript would stable and I’d love to see that implemented, I think that would resolve this thread!Hi – for what it’s worth –
We have a rather large code base (no way to know really…)
It is on the order of 450 Mocha tests.
7.x was not “snappy” to startup - but it was tolerable.
I upgraded from 7.0.2 to 8.0.1 and the tests will actually never finish on Windows. They work fine on Linux (like 7.x).
After reading through this, I was trying to use the
--files true
flag. On a smaller codebase it made a big different (the tests ran).On the code base with 450 tests… it simply never finished. The strange thing is if I set the environment variable TS_NODE_FILES –
set TS_NODE_FILES=true
; the tests start almost immediately.If it helps:
mocha --recursive --require ts-node/register -R spec ./**/*.spec.ts --no-timeouts --exit
tsconfig.json
package.json
When host doesn’t have resolveModuleName it creates the cache per program. So whenever program changes the new resolution cache is created. (Note this is different from internal resolution cache that tsc and tsserver use that are made aware of which files have changed and hence which resolutions to invalidate) I don’t think
ModuleResolutionCache
that compiler has, is equippd to handle the changes and hence its per program. I think better option would be to use our WatchAPI instead as that is equipped to handle changes in file and hence invalidating partial resolutions.@blakeembrey Hi, I also created an example: https://github.com/lukashavrlant/ts-node-perf But I guess it is similar to the previous example. Basically compiling TS files to JS files and then running mocha tests using the generated JS files is faster then using ts-node directly. See readme for more details please. The more imports I use is test files, the slower it gets. For the sake of example I used zero imports in tests files.
@eyalroth
Yeah, I believe only tsc (and ts-node without SWC) can handle
emitDecoratorMetadata
correctly. I tried almost all solutions such as babel, esbuild, swc but they doesn’t emit decorator metadata correctly. I checked it by reading compiled code and it seems impossible.Thus, I stopped using decorators. Moved from mikro-orm to objection, Type-GraphQL to Nexus, remove tsyringe, then moved from ts-node to esbuild. Now things became much better.
@thousandsofraccoons ts-node’s “swc” integration is the recommended solution here. It skips typechecking and uses the blazing-fast swc transpiler. You can use it today, check the docs links below for instructions.
https://typestrong.org/ts-node/docs/performance#skip-typechecking https://typestrong.org/ts-node/docs/transpilers#bundled-swc-integration
It is promoted out of “experimental” status in our next, upcoming release. #1536
The new features mentioned above,
--transpiler
and--show-config
, have been released in v10https://github.com/TypeStrong/ts-node/releases/tag/v10.0.0
There is a dedicated discussion thread linked if you need assistance with the upgrade.
I investigated esbuild when implementing #1160 but there were performance limitations in per-file compilation. #1160 gives the same performance benefits, and people are already successfully using it, for example https://github.com/TypeStrong/ts-node/discussions/1276#discussioncomment-590910 I recommend checking it out and giving feedback in the discussion thread.
#1160 implements pluggable transformers. The swc transformer is built-in, but anyone can write a plugin to use any other transformer. This means your existing TS project configuration is all you need to use a different transformer, and you get the same benefits as ts-node: sourcemap support, ESM loader, etc.
Also, for many people,
transpileOnly
is all they need. Often they’ve made a configuration mistake: they believetranspileOnly
is enabled, but it actually is not. We implemented #1243 to make it easier to debug configuration mistakes, enabling more people to usetranspileOnly
and to better understand their configuration.@acro5piano could you explain how you do that please 🙏
I have one big project, that unfortunately I can’t share. The entry point is “start”
At the top of this file (after many imports) I have a
console.log(new Date())
Then I run
And I get the diff from there.
✅
ts-node@8.4.0
I use nodemon and tsnode
I updated to “ts-node”: “^8.6.2”, and on each restart takes a lot to compile!
With TS_NODE_FILES=true did not show any kind of improvement.
I will try downgrade to previous version “ts-node”: “^7.0.1” or 8.4.0 as @christianrondeau mentioned from 8.4.1 version breaks performance.
@kaiza set this environment variable ->
set TS_NODE_FILES=true
It should make your tests ‘go fast’
Unfortunately this definitely seems to be the case. I’m not 100% sure why in the language services this is so expensive. It appears that changing
rootFiles
results in the “project out of date” and it starts re-resolving all types and traversingnode_modules
. This is very bad in the test loading case - I need to find a way to use the previously cached resolutions over trying to re-resolve everything when the root files change. cc @TypeStrong/typescript-team @weswigham in case I’m doing something wrong.That’s intereseting,
files: false
is definitely meant to make things faster rather than slower. Does anyone have a large enough project that’s running slow and want to either 1. investigate if the reason @davidgruar mentions appears true or 2. allow me temporary access to look into further myself?Ideally
files: false
loads a single file and spiders out so it is possible that this is a slight issue with performance. It’s also possible that the previous version was hitting the cache heavily, if you upgraded from v7 to v8 (v8 removed the cache because of type issues). We could also investigate adding caching back but it needs to be much smarter than the previous version to solve transitive type changes.The strangest thing happened. I undid all the configuration changes I made yesterday,
npm ci
-ed, added--transpile-only
alone, and now it seems that the tests start running much faster.Sorry for all the fuss.
Awesome, thank you! Feel free to share it here or create a separate issue if that would help to focus the conversation.
Of course! I’m planning on creating such a fake repo. I just need time 😃
Compilers such as babel, swc, and esbuild do not understand type information. If type information from one file affects the emitted code from another file, it will fail to emit the code you’re expecting using these tools. TypeScript has an option
isolatedModules
which will raise typechecking errors to warn you that your code is incompatible with these tools.https://www.typescriptlang.org/tsconfig/#isolatedModules
This is an important detail to understand: these errors are raised when using TypeScript to typecheck, not when using babel, swc, or esbuild to compile your code! The assumption here is that you run typechecking as part of your automated testing even when you use a non-typechecking compiler. Think of the typechecker as a lint step.
Unfortunately, the reason I closed this issue still applies here: Without any reproductions for us to look at, we can’t keep guessing at what may or may not be happening in someone’s project. Given a reproduction, we can take a look.
@cspotcode I tried all the options – cli argument, environment variable and
tsconfig.json
🤷I… have no idea. Looks like @acro5piano knows more about this 😄
We’ve seen a lot of cases where someone thought they were using transpileOnly, but actually they weren’t. So that’s always a possibility whenever it doesn’t seem like transpileOnly is making much impact: perhaps it’s not even turned on.
Is this an issue with cross-file metadata, in other words, is it compatible or incompatible with
isolatedModules
?FYI
swc
seems to be incompatible with sequelize-typescript. I found the issue sec#1160 which mentions this, but it’s possible that there are more undocumented issues preventing the two from working together.Running with
--transpile-only
alone doesn’t seem to help much for our tests execution.Could
esbuild-register
be an alternative solution?https://github.com/egoist/esbuild-register
I’m thinking to switch plain
tsc -w
andnodemon
combination with"incremental": true
.I love to use ts-node, but it tasks 8 seconds to start our API, whereas 3 seconds for plain
tsc
.@DanielSchaffer caching was removed once upon a time. There are plans to re-add it, and issues tracking that work.
@cspotcode thanks for the insight. I did some digging. It is working fine on linux. The issue is only on windows.
I believe the difference in behavior between the 2 typescripts version come from this. https://github.com/microsoft/TypeScript/pull/36011 especially https://github.com/microsoft/TypeScript/blob/0b38a9a2b03d3c651822bc2a20d381545384f0f5/src/compiler/program.ts#L566
When i track it down it is rebuilding because when typescript does
!arrayIsEqualTo(program.getRootFileNames(), rootFileNames)
it found thatC:\project\fileA.ts
is not equal toC:/project/fileA.ts
Any suggestion for a fix?
@sylc I don’t know exactly why it’s slower compared to the previous version of typescript. But I can offer some insight as to how the compiler works.
The
Program
is immutable. Whenever a file is added to compilation, a newProgram
needs to be created to include the new file. The old ASTs can be reused so that the compiler doesn’t need to re-parse them.However, the typechecker must be thrown away and a new one constructed. This means every time a file is added to the program, typechecking must be redone from scratch. (at least as I understand it) This is necessary because any file might include global type declarations or declaration merging that affects any other file of the program.
If you haven’t specified the
"types"
array in your tsconfig, it’s possible that the compiler has to load many, many@types
declarations in addition to your codebase. This can add up to a ton of typechecking.Any time it says “rebuilt Program instance” this is happening.
We can avoid this by not typechecking via
--transpile-only
mode. We can runtsc --noEmit
separately to handle typechecking instead ofts-node
.Another option is using
ts-node
’s--files
flag to eagerly load all your code at the beginning, so that the Program instance and typechecker don’t need to be rebuilt. If more files are loaded at the beginning, then they won’t be new to the Program when they’re require()d, and typechecking won’t need to be recomputed. If you manage to get this working, most of the “rebuilt Program instance” lines should go away.Hi @brunolm ,
How do you do to measure execution time?
8.4.0 works fine for me , I do not notice difference with 7.0.1 , 8.6.2 is veeeery slow. I didn’t tried the v8.7.0 yet
I’m digging into the codepath that uses the incremental compiler API. It was really slow for my reproduction (link), like a second per
require()
, because for every single change to theProgram
, it was re-parsing allSourceFile
instances. Our non-incremental codepath uses aLanguageService
, which uses aDocumentRegistry
to cache these.For the incremental compiler, we have to pass a
CompilerHost
, which has agetSourceFile
method. This was always returning a newSourceFile
instance. I don’t know if this is intended behavior or if there’s a way to cacheSourceFile
instances. (or if I’m doing something wrong in my experiments)I’m sorry, that’s our normal start script, I did add the
--transpile-only
argument to that when I was testing this.I may have found a bug in the new incremental API implementation that causes it to drop back to the non-incremental API?
builderProgram
is created viats.createIncrementalProgram
https://github.com/TypeStrong/ts-node/blob/master/src/index.ts#L590-L605 …but here, it’s reset with the legacyts.createEmitAndSemanticDiagnosticsBuilderProgram
https://github.com/TypeStrong/ts-node/blob/master/src/index.ts#L625-L632When I upgraded our project’s ts-node from 4.1.0 to 8.5.2, I also faced the above problem in that our app’s startup time significantly slowed down. As advised above, using
--transpile-only
did significantly speed up the process, but I really didn’t want to lose type-checking. So while I am waiting for a later version of ts-node to be released that will make our app build fast without having to use--transpile-only
, I have downgraded to 7.0.1. That version gives me fast build times (because, for one, it caches parts of previous build results).I’m avoiding the issue by splitting compilation and the repl, as I still want type information:
package.json (cropped)
and tsconfig.json (cropped)
then I run
npm run build:dev
in one terminal split,npm start
in the other.That’s my workaround until this is fixed 🤷♂️
@blakeembrey looks good when using this in our
package.json
:I can confirm a big performance difference between 8.4.1 and 8.4.0
my start script
and tsconfig
It’s not a solution, but downgrade to 6.2.0 helped me currently with the test performace issues.
Ok, perfect. It does look like TypeScript makes 4x the number of requests to the filesystem when
files: false
. Looking into whether there’s some issue with how TypeScript is functioning here and how this could be improved.