A Practical Guide to Frontend Package Managers
Choosing the right package manager is crucial for efficient dependency management, but each package manager has it's pros/cons and quirks.
What is a package manager?
A package manager is a tool that simplifies the process of managing and installing external libraries, frameworks, and dependencies required for a web application. It automates the retrieval, installation, and version control of these packages. This allows developers to focus on building their application rather than manually managing dependencies.
Commonly used package managers in the front-end development ecosystem include npm (Node Package Manager), Yarn, and pnpm.
What they solve
Dependency Management
Automates installation of external libraries. Resolves and manages dependencies for efficient development.Version Control
Ensures consistent dependency versions. Allows easy updates while maintaining backward compatibility.Centralized Repository
Online catalog of packages. Simplifies package discovery, documentation access, and community ratings.Package Scripts
Defines automated tasks like building, testing, and deployment for streamlined development workflow.Custom Configuration
Tailors package manager behavior through project-specific settings in configuration files.Ecosystem and Community Support
Access to a vast ecosystem of open-source packages. Community-driven maintenance, updates, and support.
Package managers anatomy
A package manager is a crucial tool in front-end development for managing dependencies, automating tasks, and streamlining workflows. Let's explore the key components that make up the anatomy of a package manager.
package.json
The package configuration defines the project's metadata, dependencies, and scripts. The central file for package configuration is package.json
. It contains essential information such as the project name, version, description, and a list of required dependencies.
Read npm's
package.json
docs (opens in a new tab) for a detailed guide onpackage.json
.
Dependency management
The dependencies
section in package.json
specifies the required external libraries and frameworks for the project. It includes the name of each package and the corresponding version range or specific version required.
dependencies | These are necessary for the application to run correctly. They are installed in the production environment. E.g. A web application using Express framework requires the express package as a dependency to handle server-side routing. |
devDependencies | These are used during development and are not necessary for the production environment. They typically include tools, testing frameworks, and other development-related dependencies. E.g. A testing library like Jest is listed as a devDependency in package.json to enable unit testing during the development process. |
peerDependencies | These specify dependencies that must be installed by the consumer of the package. They are used when a package expects a specific version of another package to be present in the consumer's environment. E.g. A library my-library requires a specific version of react to be installed in the consumer's project. By listing react as a peerDependency in package.json , my-library ensures that the correct version of react is installed, allowing for compatibility and proper functioning of the library. |
Semantic versioning
Semantic Versioning (opens in a new tab) (SemVer), is a versioning scheme following a format of MAJOR.MINOR.PATCH
to indicate backward-compatible changes, new features, and bug fixes, respectively. SemVer allows developers to define version ranges or specify exact versions for dependencies to ensure compatibility and manage updates effectively.
For example:
^1.2.3
specifies a range where any compatible version starting from 1.2.3 up to, but not including, 2.0.0 is allowed.~1.2.3
specifies a range where any compatible version starting from 1.2.3 up to, but not including, 1.3.0 is allowed.1.2.3
specifies an exact version, ensuring the project uses that specific version of the dependency. SemVer helps package managers resolve dependencies accurately, ensuring that compatible versions are installed while avoiding breaking changes or conflicts.
Scripting capabilities
The scripts
section in package.json
allows developers to define custom commands that automate various tasks. These scripts can be executed via the package manager's CLI, enabling convenient build processes, testing, linting, and deployment.
Dependency resolution
Package managers handle dependency resolution to ensure consistent and compatible versions of packages within a project.
By effectively utilizing dependency trees and lock files, package managers ensure reliable and consistent resolution of dependencies, leading to stable and reproducible development environments.
Dependency tree
The dependency tree represents the hierarchical structure of the project's dependencies and their interdependencies. It visualizes how packages rely on one another and helps manage conflicts and version constraints.
Consider the following example of a simplified dependency tree:
- Project
- Package A@1.0.0
- Package B@^2.0.0
- Package C@^1.1.0
- Package D@~2.5.0
- Package E@^3.0.0
Lock files
Lock files, such as package-lock.json
(used by npm), pnpm-lock.yaml
(used by pnpm), or yarn.lock
(used by Yarn) play a vital role in dependency resolution. They record the exact versions of the installed packages and their dependencies, ensuring reproducibility and preventing unexpected changes during subsequent installations.
Here's an example snippet:
{
"name": "project-name",
"version": "1.0.0",
"lockfileVersion": 2,
"dependencies": {
"package-a": {
"version": "1.0.0",
"requires": {
"package-b": "^2.0.0"
}
},
"package-b": {
"version": "2.1.1",
"requires": {
"package-c": "^1.1.0"
}
},
"package-c": {
"version": "1.2.3"
}
}
}
Lock files ensure that subsequent installations of the project's dependencies are based on the same versions, avoiding inconsistencies and unexpected behavior due to version changes.
Package installation
Package managers handle the installation and retrieval of packages from a central repository.
Registry
The package manager's registry serves as a centralized repository for packages. It contains a vast collection of open-source libraries and frameworks that developers can access and include in their projects. The registry allows package managers to retrieve and download packages, resolve dependencies, and manage versioning.
For example:
- npm (Node Package Manager) uses the npm registry (registry.npmjs.org) by default.
- Yarn uses the Yarn registry (registry.yarnpkg.com) by default.
- pnpm uses the npm registry (registry.npmjs.org) by default.
Cache
Package managers often maintain a local cache of downloaded packages. This cache speeds up subsequent installations and reduces reliance on network requests by storing the packages locally. When a package is required, the package manager checks the cache first before fetching it from the registry.
Command-Line interface
The package manager's CLI provides a command-line interface to interact with the tool's functionality.
CLI commands
Package managers offer a variety of commands to perform common tasks such as installing packages, updating dependencies, executing scripts, and managing the package manager's configuration. For example npm install
, or yarn upgrade
.
Check out the CLI Commands cheatsheet for a quick reference.
Customization
The CLI allows developers to customize the behavior of the package manager using configuration files. These files enable developers to define settings such as package sources, authentication credentials, and proxy configurations.
For example, npm uses a .npmrc
file to customize the configuration. Developers can specify settings like the registry URL, authentication tokens, and proxy configurations in this file. Similarly, Yarn uses a .yarnrc
file for configuring various options.
Here's an example of an .npmrc
file:
# Custom registry source
registry=https://my-custom-registry.com/
# Authentication token
//my-custom-registry.com/:_authToken=1234567890
# Proxy settings
https-proxy=http://proxy.example.com/
http-proxy=http://proxy.example.com/
By modifying these configuration files, you can tailor the package manager's behavior to your project's requirements, such as using a custom registry, applying authentication tokens, or configuring proxy settings.
Comparing npm, Yarn, and pnpm
Package managers have played a vital role in the development and management of software projects over the years. Let's explore a brief history of package managers and their evolution, focusing on three popular package managers: npm, Yarn, and pnpm.
npm
npm (opens in a new tab) (Node Package Manager) was created in 2010 as the default package manager for Node.js. It is the most widely used in the frontend community. It comes bundled with Node.js and provides a vast ecosystem of packages. Installing dependencies with npm is as simple as running npm install.
It quickly gained popularity, largely due to the rise of Node.js as a popular runtime environment for JavaScript. The npm registry became the go-to source for open-source packages, contributing to the growth of a vast ecosystem of libraries and frameworks.
Pros
Widely adopted package manager with a large ecosystem of packages.
Default package manager for Node.js and widely used in the frontend community.
Active community support and frequent package updates.
Yarn
Yarn (opens in a new tab) was developed by Facebook in 2016 (although it is no longer operated by Facebook (opens in a new tab)) and emerged as a response to certain limitations and challenges faced by npm.
Yarn introduced parallel package installations, significantly improving installation speed. It brought deterministic dependency resolution via a lock file (yarn.lock
) that ensured consistent installations across different environments. It also introduced caching dependencies, Yarn enabled offline installations, allowing developers to install packages without an internet connection. And lastly it brought Plug-and-Play (PnP), enabling packages to be used without explicit installation, reducing disk space usage and speeding up the development process.
Read yarn vs. npm (opens in a new tab) for a full description
Pros
Optimized for performance with parallel installations and a global package cache.
Workspaces support for managing multiple packages within a single repository.
Plug-and-Play (PnP) feature enables packages to be used without explicit installation, reducing disk space usage.
pnpm
pnpm (opens in a new tab) was introduced in 2016 and differentiates itself by optimizing disk space usage and installation time. It also provides excellent support for monorepos and workspaces, making it efficient to manage multiple packages within a single repository, and unlike npm and Yarn, pnpm is registry-agnostic, allowing developers to configure and use any compatible registry as their package source.
Original versions of both yarn
and npm
stored dependencies in a project-specific node_modules
directory. This means that when multiple projects have the same or overlapping dependencies, each project ends up with its own copy of those dependencies,leading to significant duplication of packages.
Newer versions of yarn
tackle this through their
Plug'n'Play (opens in a new tab) feature.
Pros
Unique approach with shared dependencies to reduce disk space usage.
Faster installation time through the use of hard links and symlinks.
Excellent support for monorepos and workspaces.
Registry-agnostic, allowing configuration and usage of any compatible registry as a package source.
Read about pnpm's significant speed advantage
In contrast to yarn
and npm
's approach using node_modules
, pnpm
introduced a shared package store approach. Instead of duplicating packages across projects, pnpm
maintains a central store of packages that can be shared among multiple projects. This is achieved by using symbolic links (opens in a new tab) to reference the packages from the shared store.
The node_modules
duplication becomes even more problematic when the doppelganger issue (opens in a new tab) is introduced.
For example, consider yarn
and npm
's approach using node_modules
. Here two projects, Project A and Project B, with the same dependency, "Example Package":
Project A
├── package.json
└── node_modules
└── example-package
Project B
├── package.json
└── node_modules
└── example-package
pnpm however has a shared store of packages, and uses symbolic links to reference the packages from the shared store:
Shared Package Store
└── example-package
Project A
├── package.json
└── node_modules
└── .pnpm
└── example-package -> ../../Shared Package Store/example-package
Project B
├── package.json
└── node_modules
└── .pnpm
└── example-package -> ../../Shared Package Store/example-package
Feature comparison
Feature | npm | Yarn | pnpm |
---|---|---|---|
Popularity | High | High | Growing |
Ecosystem | Large | Large | Expanding |
Dependency Management | package.json | package.json | package.json |
CLI | Comprehensive | Similar to npm | Similar to npm |
Package Scripts | ✅ | Not specific | Not specific |
Registry | npm registry | Yarn registry | Customizable |
Speed | Moderate | Fast | Very Fast |
Lock File | package-lock.json | yarn.lock | pnpm-lock.yaml |
Performance | Moderate | Fast | Very Fast |
Disk Space | Moderate | Moderate | Low |
Offline Support | Limited (with npm ci ) | ✅ | ✅ |
Parallel Installs | ❌ | ✅ | ✅ |
Workspaces | ✅ (since npm 7) | ✅ | ✅ |
Plug-and-Play (PnP) | ❌ | ✅ | ❌ |
Monorepo Support | ❌ | ✅ | ✅ |
Community Support | Active | Active | Growing |
Keep up to date with any latest changes or announcements by subscribing to the newsletter below.