Articles

Why Package Installs Are Hard in Browser Runtimes

Package installation is where browser runtimes meet real npm behavior: lockfiles, tarballs, native packages, lifecycle scripts, and performance.

Running node index.js in a browser runtime is only the beginning. The harder question is what happens when the project depends on real npm packages.

Package installation looks simple from the outside: read package.json, download packages, write node_modules. In practice, modern JavaScript package managers encode a large amount of platform behavior. A browser runtime has to decide which parts to emulate, which parts to optimize, and which parts to reject clearly.

The browser is not a normal install target

Most packages were published with an implicit host in mind. They expect a filesystem, process environment, lifecycle scripts, binaries, symlinks, platform filters, and sometimes native addons. A browser runtime can provide many of those semantics, but not all of them.

The awkward cases include:

Ignoring those cases makes demos flaky. Treating every case as supported makes the runtime dishonest. The better path is a compatibility contract: support the common path, optimize it, and report the unsupported path early.

Lockfiles are the source of truth

For embedded runtimes, the fastest credible default is lockfile hydration. Let npm, pnpm, or Yarn resolve the dependency graph and record the exact package layout. Then hydrate that layout inside the browser runtime.

That avoids making the browser perform every resolver decision from scratch. It also gives the host product reproducibility: the same lockfile should create the same dependency tree.

Hydration still has to do real work:

The lockfile gives structure. It does not eliminate the install problem.

Tarballs are expensive in the hot path

Downloading and unpacking npm tarballs in the tab can be slow. Tarballs were designed for package distribution, not repeated interactive demo startup inside a browser.

That is why a browser runtime benefits from a registry proxy and bundle format. A proxy can pre-process package metadata, expose browser-friendly package bundles, and avoid making each visitor repeat the same expensive archive work.

The install path then becomes:

That keeps the browser path cheap without pretending the public npm registry was built for this use case.

Native packages need explicit policy

Native packages are where browser runtimes need to be direct. If a package requires a .node addon, the browser cannot load it. If a tool requires a native binary, the browser cannot execute it as a Linux process.

There are three reasonable outcomes:

What is not reasonable is a vague install failure after the user has waited. The runtime should name the incompatible package and explain why the browser backend cannot run it.

Lifecycle scripts are a product decision

Lifecycle scripts are powerful and dangerous in embedded environments. They can compile native code, fetch extra assets, mutate files, or assume shell behavior that the browser runtime does not implement.

For many demos and tutorials, the right path is to avoid lifecycle scripts in the browser install path and rely on prebuilt package content. For advanced workflows, scripts may belong on the server backend.

That split is not a weakness. It is the same browser-first rule applied to installs: run the cheap deterministic path in the tab, promote when the workload requires server semantics.

Good package installs are mostly invisible

The user should not think about any of this during a tutorial or product demo. They should open a page, see the project mount, run the command, and get a preview.

That invisibility takes engineering work. It requires a clear install mode, path validation, package cache behavior, native package policy, useful diagnostics, and a server fallback. Package installs are hard in browser runtimes because they are where the browser sandbox meets the real npm ecosystem.