Running Node.js natively on Apple Silicon

Published
2021-01-14
Tags
PerformanceAppleDevOps

I just got the one of the newest M1-based MacBooks and I wanted to get it up and running as fast as possible so I went with the Migration Assistant and migrated everything from my old MacBook Pro 2017 running macOS 10. To my surprise it worked without any issues at all (I would expect at least it requires same OS versions).

Although it already felt much faster than my previous machine even running most of the software through Rosetta 2 emulation mode, I was still curious: how much faster it actually is when running things natively.

Comparing Chrome via Rosetta 2 and native arm64 build.
Comparing Chrome via Rosetta 2 and native arm64 build.

Since the existing Homebrew installation wouldn't allow my updating or installing packages anymore because of more restrictive OS permissions, I was faced with "update the x86 homebrew" or “use an experimental cutting-edge ARM build”. I went with the latter and this post summarizes my experience.

Step 0. Remove x86 zsh binary

One of the biggest confusion for me was the fact my shell would not run in arm64 mode. This was caused by the fact I already have been using zsh shell before and it was migrated over. The new macOS Big Sur comes with zsh as a default shell so you don’t need to install it separately anymore.

To solve this issue I had to remove x86 compiled zsh that I installed via Homebrew. This cannot be done with brew remove since Homebrew won’t have enough permissions but you can remove zsh manually:

// 1. verify you're not running native zsh

which zsh

// 2. if it is different from /bin/zsh, run

rm "$(which zsh)"

Verify by opening a new terminal session and running arch.

➜ arch
arm64

Step 1. Install Apple Silicon version of the Homebrew

To install the ARM version of Homebrew on the Apple Silicon Mac, I used the manual "untar anywhere" method: https://docs.brew.sh/Installation#untar-anywhere

💡
Starting from version 2.7.1, Homebrew supports Apple Silicon out-of-the-box.

This method allows installing Homebrew alongside with the x86 version which is probably a good idea for a time being since not all packages are yet pre-built for ARM. I had a few issues while building from sources, too.

After installing Homebrew into separate directory, add the following to your .zshrc file

# Support for two Homebrew installations
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
alias ibrew='arch -x86_64 /usr/local/bin/brew'

After that you can use both arm64 and x86 installations of Homebrew side-by-side.

For more information on Homebrew compatibility on Apple Silicon check out https://github.com/mikelxc/Workarounds-for-ARM-mac and https://soffes.blog/homebrew-on-apple-silicon

Step 2. Install node and yarn

After you have installed Homebrew, install node (which includes npm) normally running brew install node Install Yarn using brew install yarn.

To verify that you're running the Node in ARM architecture, enable the row "Architecture" in Activity Monitor,

Enable "Architecture" column with the right-click on the header. If node processes say "Apple" you're running them natively!
Enable "Architecture" column with the right-click on the header. If node processes say "Apple" you're running them natively!

Step 3. Install packages to run Gatsby

After running Node natively I had issues running Gatsby project. While doing npm install in the Gatsby project, it will fail because of the binding for the Sharp — a native C library Gatsby using — needs to be compiled under the ARM architecture. It didn't work out of the box for me since some native packages were missing. I was able to resolve it following this GitHub issue https://github.com/lovell/sharp/issues/2460 and doing

cd ~/gatsby-project
brew install vips
rm -rf node_modules
npm install

After that the installation went well and I could run the project. There were no issues whatsoever running other JavaScript projects (using TypeScript or Bable).

So, how much faster is ARM Node compared to x86

I ran a few tests on the same machine and also compared build speeds with my previous MacBook Pro 2017 that has 2,9 GHz i7 Quad Core CPU and 16 GB of RAM.

Building the Next.js for
Building the Next.js for
vs. 17 seconds when using Rosetta2 emulation and x86 Node.
vs. 17 seconds when using Rosetta2 emulation and x86 Node.

The results were... astonishing!

Running x86 vs arm64 node resulted in a ~40% improvement. Building a Gatsby site with a few dozens of pages went from 55 seconds on MacBook Pro to 15 seconds on MacBook Air!
54 seconds to build a Gatsby site on MacBook Pro 2,9 GHz i7 CP
54 seconds to build a Gatsby site on MacBook Pro 2,9 GHz i7 CP
vs 15 seconds of build time on MacBook Air M1 2020
vs 15 seconds of build time on MacBook Air M1 2020

Let's not forget, MacBook Air is a fan-less machine that you can put on your lap without a risk of getting a skin burn. And my old computer was making noises similar to an airplane taking off 🤦‍♂️.

Here is the related Twitter thread: https://twitter.com/okonetchnikov/status/1346162117506985985

Next steps

I still didn't finish this, but I'd like to try cleaning up previous installation of Homebrew completely and move everything to /opt/homebrew and also try installing only native packages. It didn't work for me for fnm that I'm using to manage node versions meaning if I install a different node version over it it will use x86 build again.

Automating installation

In order to save time in the future, I have a collection of scripts that automate the process of setting up a new computer. You can find them onGitHub https://github.com/okonet/dotfiles. I've updated it accordingly so it should work on M1 Macs. Use it your own risk and please report issues and send PRs if you decide to use those scripts.