Introduction
Last month, our team shaved 4 minutes off every CI/CD build by switching from npm to pnpm. That might not sound like much until you consider we run about 200 builds per week—suddenly we’re saving 13+ hours of CI time monthly. For a development team billing at $150/hour, that’s not nothing.
The JavaScript ecosystem has matured significantly since npm became the default package manager, and if your mobile development team is still using vanilla npm in 2025, you’re probably leaving performance (and developer happiness) on the table. Let’s dig into why modern package managers matter for mobile app development and how to make the switch.
The Problem with Traditional npm
npm has served the JavaScript community well since 2010, but its architecture shows its age. Every project gets its own copy of every dependency, creating massive node_modules folders. On a typical React Native project, you might see 500MB+ of dependencies, much of it duplicated across projects.
For mobile development specifically, this creates several pain points:
Build times suffer. Every npm install downloads and extracts everything from scratch. On a fresh clone, you’re waiting 2-3 minutes minimum, often much longer.
Disk space explodes. Working on multiple mobile projects? Each one maintains its own complete copy of React Native, Expo, and every transitive dependency.
CI costs add up. Cloud CI providers charge by the minute. Those slow installs hit your budget directly.
Monorepo support is clunky. npm workspaces work, but they feel bolted-on rather than native.
Enter the Modern Package Managers

Three alternatives have emerged as serious contenders: pnpm, Yarn (specifically Yarn Berry with PnP), and Bun. Each takes a different approach to solving npm’s problems.
pnpm: The Practical Choice
pnpm uses a content-addressable store and hard links to avoid duplication. Every package version exists once on your disk, and projects link to that single copy.
Here’s what this looks like in practice. With npm:
# npm creates this structure
node_modules/
├── react-native/ # 150MB
├── @react-navigation/ # 20MB
├── expo/ # 80MB
└── ...thousands more
With pnpm:
# pnpm creates symlinks to a global store
node_modules/
├── .pnpm/ # Flat structure with symlinks
├── react-native -> ~/.pnpm-store/[email protected]
└── ...
The result? Our React Native project went from 847MB in node_modules to 234MB with pnpm—a 72% reduction. And because packages are stored globally, your second project with the same dependencies installs in seconds, not minutes.
Yarn Berry: The Ambitious One
Yarn’s Plug’n’Play (PnP) mode takes a more radical approach: it eliminates node_modules entirely. Instead, Yarn generates a .pnp.cjs file that tells Node.js exactly where to find each package.
// .pnp.cjs (simplified)
// Maps package requests to their locations
["react-native", [
["npm:0.74.0", {
"packageLocation": "./.yarn/cache/react-native-npm-0.74.0-abc123.zip",
"packageDependencies": [/* ... */]
}]
]]
This is elegant in theory, but mobile development throws a wrench in the works. Many React Native libraries assume node_modules exists. Metro bundler, the default for React Native, needs configuration tweaks to work with PnP. It’s doable, but you’ll spend time on compatibility issues.
Bun: The Fast One
Bun is a JavaScript runtime that includes a package manager, and it’s genuinely fast. Like, absurdly fast. In our benchmarks, bun install completed in 3.2 seconds what took npm 47 seconds.
# Timing comparison on our React Native project
npm install: 47.3s
yarn install: 34.1s
pnpm install: 12.8s
bun install: 3.2s
The catch? Bun is still maturing for mobile development. React Native’s Metro bundler doesn’t officially support Bun yet (you’ll use Bun for package management, Node for running Metro). Some native modules have quirks. It works, but you’re on the bleeding edge.
Practical Migra
tion Guide
For most mobile teams, we recommend pnpm as the balance of speed, compatibility, and stability. Here’s how to migrate:
Step 1: Install pnpm
# macOS/Linux
curl -fsSL https://get.pnpm.io/install.sh | sh -
# Or with npm (ironic, but works)
npm install -g pnpm
# Verify installation
pnpm --version
Step 2: Configure for React Native
Create or update .npmrc in your project root:
# .npmrc
node-linker=hoisted
shamefully-hoist=true
The shamefully-hoist flag (yes, that’s really what it’s called) creates a flatter node_modules structure that React Native and Metro expect. Without it, you’ll get resolution errors.
Step 3: Migrate Your Project
# Remove old node_modules and lockfile
rm -rf node_modules
rm package-lock.json # or yarn.lock
# Install with pnpm
pnpm install
pnpm generates its own pnpm-lock.yaml. Commit this file and add package-lock.json to .gitignore.
Step 4: Update Your Scripts
Most scripts work unchanged, but update CI configurations:
# GitHub Actions example
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
Step 5: Handle Edge Cases
Some React Native libraries expect specific node_modules structures. If you hit resolution errors, add overrides to your .npmrc:
# .npmrc additions for stubborn packages
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
public-hoist-pattern[]=@babel/*
Monorepo Benefits
If your mobile project shares code with a web app or backend—common in modern development—pnpm’s workspace support is excellent.
# pnpm-workspace.yaml
packages:
- 'apps/mobile'
- 'apps/web'
- 'packages/shared'
- 'packages/ui'
Shared packages are automatically linked. Update your shared UI library, and both mobile and web apps see the changes immediately:
# Structure
my-app/
├── apps/
│ ├── mobile/ # React Native
│ └── web/ # Next.js
├── packages/
│ ├── shared/ # Business logic
│ └── ui/ # Shared components
├── pnpm-workspace.yaml
└── package.json
Real-World Impact
We migrated three client projects to pnpm last quarter. Here’s what we saw:
Project A (React Native + Expo):
- npm install: 3m 12s → pnpm install: 48s
- node_modules: 1.2GB → 380MB
- CI build time: 8m 45s → 5m 20s
Project B (React Native bare workflow):
- npm install: 2m 47s → pnpm install: 41s
- node_modules: 847MB → 234MB
- CI build time: 7m 10s → 4m 55s
Project C (Monorepo with mobile + web):
- npm install: 4m 33s → pnpm install: 1m 02s
- Total node_modules: 2.8GB → 890MB
- CI build time: 12m 30s → 7m 15s
Across all projects, developer feedback was positive. The main comment: “I didn’t realize how much time I spent waiting for npm until I stopped waiting.”
When to Consider Alternatives
pnpm is our default recommendation, but consider the alternatives in specific cases:
Choose Bun if:
- You’re starting a new project with Node 20+
- Your team is comfortable with early-adopter challenges
- Raw speed is the priority over compatibility
Choose Yarn Berry (PnP) if:
- You’re not using React Native (web projects work great)
- You want zero-install commits (dependencies in git)
- Your team can invest in tooling compatibility
Stick with npm if:
- You’re on a legacy project with complex native dependencies
- Your CI caches npm specifically and migration cost exceeds benefit
- Your team is small and install times aren’t a bottleneck
Common Migration Issues and Fixes
Issue: Metro bundler can’t resolve packages
Solution: Ensure shamefully-hoist=true is set. If that doesn’t work, check that the failing package is hoisted:
pnpm why package-name
Issue: Native module linking fails
Solution: Clear caches and reinstall:
rm -rf node_modules
rm -rf ios/Pods
pnpm install
cd ios && pod install
Issue: Peer dependency warnings
Solution: pnpm is stricter about peer deps. Add explicit resolutions:
{
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"react": "18"
}
}
}
}
Conclusion
Switching package managers isn’t the most exciting part of mobile development, but it compounds. Every minute saved on npm install adds up across your team, across builds, across months of development.
For most mobile teams in 2025, pnpm offers the best balance: it’s fast, compatible with React Native and Expo, handles monorepos well, and saves significant disk space. The migration takes an afternoon, and you’ll wonder why you didn’t switch sooner.
Start with a low-stakes project, validate everything works, then roll it out to your main codebase. Your CI bills and your developers will thank you.