In-browser "npm install" benchmark

Creating an in-browser tool to deal with a node_modules file tree such as "running Webpack in browser" often requires essentially running "npm install" in browser. This benchmark is to compare that performance across alternatives, which is WebContainer (by StackBlitz), Nodebox (by CodeSandBox), and npm-in-browser.

I (Naru) built npm-in-browser as an open-source alternative way to run "npm install" in browser without relying on black-box runtime / CDN and wanted to see whether it's practical to be used for creating tools such as JS Bundle Lab.

In summary, the benchmark has shown that npm-in-browser is performant enough to be used to do "npm install" typical frontend packages such as react-dom. Interestingly, it was faster than WebContainer in this case. For heavy packages such as installing typescript, WebContainer was faster but npm-in-browser was not extremely slow. Note that Nodebox was always extremely faster than the others in all the settings. I believe this is because Nodebox is using highly optimized format to deliver npm packages via their own CDN and they also use their own algorithm of "npm install", which does not seem to be using the actual "npm" CLI.

Note that as of 2023-10-18, both WebContainer and Nodebox have some restrictions on its usage such as commercial usage and their frontend/backend source code is not public. npm-in-browser is just a small open-source library without commercial usage restrictions.

Benchmark setting

I ran the benchmark on using my MacBook Air (M2, 2022) under a high-speed internet connection, which should represent a typical environment at home in Japan.

The rules are the following:

I used Puppeteer to measure "cold startup time" and "warm startup time". For each way, the same browser instance loaded the same page 6 times in succession and the first one is considered as "cold startup time" and the remaining 5 are considered as "warm startup time". I repeated this 100 times to plot 100 points in "cold startup time" and 500 points in "warm startup time" for each way.

Note that I did not use network throttling / CPU throttling since I was not confident that it is working correctly for everything including Web Workers, WASM, and Service Workers.

npm-in-browser and in-memory file systems

npm-in-browser requires us to supply Node.js fs compatible instance and it turned out that the performance of this fs matters. In this benchmark, I used memfs and "custom-fs", which is a custom in-memory filesystem I built just to make npm-in-browser work. Note that this "custom-fs" is not battle-tested and it has many edge case bugs since I only implemented absolutely necessary parts. Also note that memfs is order of magnitude slower without a setImmediate polyfill. All the benchmarks using memfs here are using the polyfill.

Benchmark results

Installing lightweight frontend packages

In this case, it installs lightweight frontend packages essentially by running npm install react-dom@18.2.0 react@18.2.0 zod@3.22.2 framer-motion@10.16.4. This should represent a situation where we want to create tools like "frontend bundle size checker" such as JS Bundle Lab.

We see that for this case npm-in-browser performs better than WebContainer although its variance is higher.

Cold startup time

Time to finish "npm install" since visiting the page, without browser cache. This should approximates the experience for first-time visitors.

Nodebox
median: 510 ms, mean: 613 ms, stddev: 358 ms
npm-in-browser with custom-fs
median: 2,005 ms, mean: 2,586 ms, stddev: 1,302 ms
npm-in-browser with memfs
median: 2,066 ms, mean: 2,836 ms, stddev: 1,476 ms
WebContainer
median: 3,329 ms, mean: 3,485 ms, stddev: 516 ms

Warm startup time

Time to finish "npm install" since reloading the page. For the most of files, browser cache should be used. This should approximates the experience for revisitors.

Nodebox
median: 211 ms, mean: 222 ms, stddev: 62 ms
npm-in-browser with custom-fs
median: 769 ms, mean: 788 ms, stddev: 140 ms
npm-in-browser with memfs
median: 833 ms, mean: 849 ms, stddev: 115 ms
WebContainer
median: 1,778 ms, mean: 1,811 ms, stddev: 303 ms

Installing heavyweight build packages

In this case, it installs heavyweight build packages essentially by running npm install typescript@5.2.2 webpack@5.88.2 @babel/core@7.23.0 next@13.5.4. We might want to install this type of packages when we want to replicate Node.js build environment in a browser.

We see that Nodebox is extremely fast and npm-in-browser is slower than WebContainer. We also see that for npm-in-browser, replacing memfs with a custom simpler in-memory fs ("custom-fs") improved the performance.

Cold startup time

Time to finish "npm install" since visiting the page, without browser cache. This should approximates the experience for first-time visitors.

Nodebox
median: 2,649 ms, mean: 2,911 ms, stddev: 765 ms
npm-in-browser with custom-fs
median: 9,846 ms, mean: 13,615 ms, stddev: 6,676 ms
npm-in-browser with memfs
median: 11,575 ms, mean: 15,672 ms, stddev: 7,169 ms
WebContainer
median: 8,881 ms, mean: 9,568 ms, stddev: 1,652 ms

Warm startup time

Time to finish "npm install" since reloading the page. For the most of files, browser cache should be used. This should approximates the experience for revisitors.

Nodebox
median: 504 ms, mean: 539 ms, stddev: 323 ms
npm-in-browser with custom-fs
median: 3,072 ms, mean: 3,319 ms, stddev: 1,809 ms
npm-in-browser with memfs
median: 4,397 ms, mean: 4,788 ms, stddev: 2,259 ms
WebContainer
median: 2,556 ms, mean: 2,530 ms, stddev: 249 ms