Avoiding TypeScript compilation in Node
Published on: Mon Dec 19 2022
Introduction
Unlike newer JavaScript runtimes, such as Deno or Bun, Node does not run TypeScript natively. That means, that you have to transpile TypeScript files first to JavaScript to be able to run them.
$ node src/index.ts # This doesn't work
$ tsc && node src/index.js # This works
Personally, I hate that intermediate building step, especially when I’m sharing code between packages or not have automatic CI/CD pipelines setup. I much rather like using Deno for TypeScript projects, but sometimes Node just offers the better architectural echosystem (like Hosting and Serverless), that force me to use Node over Deno.
I also ran into this “problem” in a project of mine, where I had a monorepository and I was coding up an API for a new feature. The whole project was written in TypeScript, which wasn’t a problem, because the production components were 2 NextJS applications, for which I had CI/CD setup, so I didn’t have to worry about TypeScript compilation. But for architectural reasons this API was going to run on a VPS and managed manually.
I used fastify for the HTTP server and setup a couple of scripts
in the package.json
.
- A
dev
script that runstsx src/index.ts
to run the - A
build
script that runstsc
to output JavaScript files - A
start
script that runsnode dist/index.js
to run the server with node directly
Pretty standard for a TypeScript application. Use a tool like tsx
or ts-node
to run the TypeScript files directly (not really obviously, but the tools hide the transpilation
step from you) to make the DX smoother and faster.
But then I was wondering… why not only keep the start
script and make it run the command
tsx src/index.ts
. That way I didn’t have to worry about forgetting to compile my TypeScript
source code when I deployed an update to the API, since I didn’t have an automatic CI/CD pipeline.
So I decided to investigate and see what the tradeoff would be, if I used a tool to run TypeScript with Node directly. Maybe it will be performance? memory usage? Let’s find out.
Methodology
You can find the code for my experiments in this repo: daniellionel01/node-ts-performance
I wanted to benchmark pure computational speed and network speed. So after some googling I found @thi.ng/bench, a benchmarking tool from an amazing open source software collection.
For the pure computational speed benchmark I used the example from the @thi.ng/bench
package, which was using
calculating fibonacci sequences.
For the network speed benchmark I used autocannon to measure and setup
a fastify api that just returns a bit of json on the /
route.
For each benchmark I measured it using tsx
, ts-node
and using regular node
with JavaScript.
Results
Here are the results of our, in total, 6 benchmarks.
Fibonacci
TSX
$ yarn fib:tsx
814.92ms
0.05ms
428.53ms
62.49ms
Title | Iter | Size | Total | Mean | Median | Min | Max | Q1 | Q3 | SD% |
---|---|---|---|---|---|---|---|---|---|---|
fib2(10) | 10 | 100000 | 51.35 | 5.13 | 4.74 | 4.62 | 7.38 | 4.69 | 5.75 | 15.83 |
fib2(20) | 10 | 100000 | 123.52 | 12.35 | 12.25 | 11.98 | 13.64 | 12.21 | 12.52 | 3.73 |
fib2(30) | 10 | 100000 | 153.01 | 15.30 | 15.37 | 15.09 | 15.51 | 15.19 | 15.45 | 1.00 |
fib2(40) | 10 | 100000 | 186.79 | 18.68 | 18.45 | 17.69 | 20.77 | 18.44 | 19.30 | 4.21 |
ts-node
$ yarn fib:ts-node
742.32ms
0.05ms
385.93ms
71.95ms
Title | Iter | Size | Total | Mean | Median | Min | Max | Q1 | Q3 | SD% |
---|---|---|---|---|---|---|---|---|---|---|
fib2(10) | 10 | 100000 | 48.98 | 4.90 | 4.59 | 4.33 | 7.76 | 4.42 | 5.04 | 19.97 |
fib2(20) | 10 | 100000 | 113.01 | 11.30 | 11.24 | 10.92 | 12.50 | 11.21 | 11.28 | 3.69 |
fib2(30) | 10 | 100000 | 147.85 | 14.79 | 14.44 | 13.93 | 17.65 | 14.33 | 15.76 | 7.18 |
fib2(40) | 10 | 100000 | 168.47 | 16.85 | 16.89 | 16.40 | 17.25 | 16.81 | 17.18 | 1.54 |
JavaScript
$ yarn fib:js
796.97ms
0.08ms
415.02ms
64.00ms
Title | Iter | Size | Total | Mean | Median | Min | Max | Q1 | Q3 | SD% |
---|---|---|---|---|---|---|---|---|---|---|
fib2(10) | 10 | 100000 | 52.86 | 5.29 | 4.96 | 4.91 | 7.61 | 4.94 | 5.60 | 15.11 |
fib2(20) | 10 | 100000 | 133.90 | 13.39 | 13.34 | 12.86 | 14.63 | 13.20 | 13.67 | 3.57 |
fib2(30) | 10 | 100000 | 168.18 | 16.82 | 16.91 | 15.91 | 17.67 | 16.79 | 17.01 | 2.52 |
fib2(40) | 10 | 100000 | 183.73 | 18.37 | 18.27 | 17.93 | 19.50 | 18.09 | 18.76 | 2.50 |
HTTP Server
TSX
$ yarn fib:bench
Running 10s test @ http://localhost:8080
10 connections
┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬───────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼───────┤
│ Latency │ 0 ms │ 0 ms │ 0 ms │ 0 ms │ 0.02 ms │ 0.14 ms │ 12 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴───────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬──────────┬─────────┬────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼─────────┼─────────┼──────────┼─────────┼────────┤
│ Req/Sec │ 17119 │ 17119 │ 29007 │ 29311 │ 27875.64 │ 3411.48 │ 17108 │
├───────────┼─────────┼─────────┼─────────┼─────────┼──────────┼─────────┼────────┤
│ Bytes/Sec │ 3.22 MB │ 3.22 MB │ 5.46 MB │ 5.51 MB │ 5.24 MB │ 641 kB │ 3.22 MB│
└───────────┴─────────┴─────────┴─────────┴─────────┴──────────┴─────────┴────────┘
Req/Bytes counts sampled once per second.
# of samples: 11
307k requests in 11.02s, 57.6 MB read
ts-node
$ yarn fib:bench
Running 10s test @ http://localhost:8080
10 connections
┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬───────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼───────┤
│ Latency │ 0 ms │ 0 ms │ 0 ms │ 0 ms │ 0.01 ms │ 0.12 ms │ 13 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴───────┘
┌───────────┬─────────┬─────────┬────────┬─────────┬─────────┬─────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┼─────────┤
│ Req/Sec │ 20623 │ 20623 │ 28719 │ 29215 │ 28067.2 │ 2494.06 │ 20614 │
├───────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 3.88 MB │ 3.88 MB │ 5.4 MB │ 5.49 MB │ 5.28 MB │ 469 kB │ 3.88 MB │
└───────────┴─────────┴─────────┴────────┴─────────┴─────────┴─────────┴─────────┘
Req/Bytes counts sampled once per second.
# of samples: 10
281k requests in 10.02s, 52.8 MB read
JavaScript
$ yarn fib:bench
Running 10s test @ http://localhost:8080
10 connections
┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬───────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼───────┤
│ Latency │ 0 ms │ 0 ms │ 0 ms │ 0 ms │ 0.01 ms │ 0.11 ms │ 15 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴───────┘
┌───────────┬─────────┬─────────┬────────┬─────────┬──────────┬─────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼────────┼─────────┼──────────┼─────────┼─────────┤
│ Req/Sec │ 22511 │ 22511 │ 32431 │ 32639 │ 31453.82 │ 2845.98 │ 22507 │
├───────────┼─────────┼─────────┼────────┼─────────┼──────────┼─────────┼─────────┤
│ Bytes/Sec │ 4.24 MB │ 4.24 MB │ 6.1 MB │ 6.14 MB │ 5.91 MB │ 534 kB │ 4.23 MB │
└───────────┴─────────┴─────────┴────────┴─────────┴──────────┴─────────┴─────────┘
Req/Bytes counts sampled once per second.
# of samples: 11
346k requests in 11.02s, 65 MB read
Conclusion
From the results from our benchmarks we can conclude the following:
ts-node
had better network performance thantsx
tsx
had better pure computational performance thants-node
node
had better performance overall, but didn’t have a humongous impact (like a x2 improvement)
My conclusion is that, for my use case, using tsx
directly is worth it. I’d rather
have a couple of % points less maximum throughput (which isn’t even relevant for this use case anyways, but in theory),
than to forget to run yarn build
and a bug staying unfixed for multiple days and hours
of debugging.
But you should definitely take my tests with a big grain of salt. If you’re evaluating the same question for your own project, run them on your own hardware, modify them, run them multiple times over the span of a couple of minutes instead of just once while spotify is running on the same laptop and using your CPU in an unpredictable manner.
Sources
- [1] https://www.section.io/engineering-education/how-to-use-typescript-with-nodejs/
- [2] https://nodejs.dev/en/learn/nodejs-with-typescript/
- [3] https://expressflow.com/blog/posts/native-typescript-in-node-js
- [4] https://github.com/TypeStrong/ts-node
- [5] https://github.com/esbuild-kit/tsx
- [6] https://github.com/thi-ng/umbrella/tree/develop/packages/bench
- [7] https://github.com/mcollina/autocannon
- [8] https://www.fastify.io/