Two weeks ago, I had opened a new tab on my terminal and complained in agony: "Oh dear god! Every time I open a new terminal it takes like one second!". My teammates looked at me funny. "This is clearly sub-optimal and hurts my focus, and I believe it's NVM's fault."
I found it's NVM that is at fault after I did a search around my
.zshrc file, checking which line takes the most time. It was NVM's initialization. I have been using NVM for years, and I always wanted to write a simple replacement, because my use cases are pretty simple:
These tasks aren't hard to solve. Node.js binaries are distributed in tarballs on their website, and switching versions shouldn't be more than just changing a symbolic link around. So, why is NVM so slow?
I thought about writing it as a simple Bash script, just like NVM, but I wanted it to be interesting and fun. Also, not all machines have Bash installed, or there might be problems integrating with Bash. I have used Fish shell for years, and in order to use NVM, I had to use a wrapper that fixes things. It wasn't easy. Using a real executable, on the other hand, would work on every shell!
My first prototype was a TypeScript app. I packaged it with Zeit's pkg, making it a self-contained executable, because I didn't want to have dependency on Node. I wanted it to work on a system without Node installed (so the first version of Node would be installed using fnm!)
Node's startup time wasn't good enough for me. Just spawning a "hello world" takes around 200ms, which is good for servers, and for command line utilities that you don't use frequently, maybe, but some people use
nvm on every
cd, so their Node version will always be in sync. 200ms penalty for every
cd is madness and would make the tool unusable.
So it seems like I need to write it in a language that is compiled (so no dependencies on the host system), and with a fast boot time. Four languages came to mind: Go, Rust, Crystal and Reason/OCaml.
I chose Reason for many Reasons (hehe), some of them are written in another post. I had used
pesy, two awesome packages that make the development workflow for native Reason/OCaml apps easy-peasy for Node.js developers.
esy works like a super-powered
yarn: it installs packages from npm or OPAM (OCaml package manager) and stores it in a global cache. It also manages a sandbox for the OCaml runtime/dependencies for you, so different OCaml installations won't interrupt each other.
pesy generates build configurations for Dune, OCaml's build tool, right from the
When using both packages, it feels just like Node development -- everything works with a single
I had built the first prototype, and tested its performance. I had two test files, one using NVM and one using fnm. Both tests spawned a plain Bash, called the "initialization" of the target (NVM/fnm), and then switched to the Node version specified in
.nvmrc file in the directory using the target. I ran it 10 times for each binary, and the results were amazing:
So in that test, on my MacBook Pro, fnm was 40 times faster than NVM. That's pretty huge, despite not being very scientific.
When I had started working on fnm, I joined the Reason Discord server, and asked some help from people. I found that community is so nice. When I decided to release, I wrote a short message on the
#native-development channel and immediately got great feedback, telling me it's a great idea.
I released fnm to GitHub as open source, tweeted about it and went to bed. When I woke up,
That's a big deal.
#1 on HackerNews!
fnm has some features still missing. Here are only a few things we want to add:
So if you feel like you're ready to use it and start working faster, or you're a JS/Reason developer willing to contribute to an open source project, download fnm and join us on GitHub!