Чем pnpm отличается от npm и yarn?

JuniorFrontend инструменты #tooling #package-managers

Вопрос

В чём разница между npm, yarn и pnpm и почему pnpm экономит место на диске?

Короткий ответ

npm и yarn (в классическом режиме) кладут копию каждого пакета в плоский node_modules для каждого проекта, дублируя одни и те же версии на диске. pnpm хранит каждый пакет один раз в глобальном content-addressable хранилище и линкует его в проекты через симлинки/хардлинки, что экономит место и делает структуру node_modules строгой — без "фантомных" зависимостей.

Подробный ответ

npm и yarn (classic) используют плоский node_modules: зависимости и их поддерево поднимаются (hoisting) на верхний уровень, чтобы избежать дублирования и глубокой вложенности. Побочный эффект — фантомные зависимости: пакет A может импортировать пакет B, который реально ему не объявлен в package.json, а просто оказался рядом на верхнем уровне из-за того, что его поставил пакет C. Код работает, но ломается при любом изменении дерева зависимостей.

pnpm решает это иначе:

  • Все версии всех пакетов физически хранятся один раз в глобальном content-addressable store (обычно ~/.pnpm-store), адресуемом по хэшу содержимого.
  • В node_modules каждого проекта pnpm создаёт симлинки/хардлинки на этот store, а не копирует файлы.
  • Структура node_modules/.pnpm — не плоская: каждый пакет видит через симлинки только те зависимости, что реально объявлены в его package.json. Это даёт строгую изоляцию: если код импортирует необъявленный пакет, он просто не найдётся — ошибка проявится сразу, а не позже в проде или у другого разработчика.
  • За счёт хардлинков на одну и ту же физическую копию файла экономия места особенно заметна в монорепозиториях и при большом числе проектов с одинаковыми зависимостями — одна версия react физически лежит на диске один раз, сколько бы проектов её ни использовали.

Yarn Berry (2+) тоже пытается решить проблему hoisting'а через режим Plug'n'Play (без node_modules вообще, резолвинг через .pnp.cjs), но это менее совместимо с экосистемой, поэтому многие остаются на node-modules linker.

С точки зрения командного API все три менеджера сегодня похожи (install, add, run), различия — в lock-файлах (package-lock.json, yarn.lock, pnpm-lock.yaml) и в скорости/дисциплине резолвинга.

Пример

# npm / yarn classic — плоский node_modules, возможны фантомные зависимости
npm install lodash

# pnpm — пакет физически один раз в сторе, в проекте — симлинки
pnpm add lodash

# посмотреть, где реально лежит store
pnpm store path
# C:\Users\User\AppData\Local\pnpm\store\v3

# pnpm сразу подсветит проблему, если код импортирует
# пакет, не объявленный в package.json своего модуля —
# он просто не будет доступен через строгие симлинки
// package.json — команды одинаковы независимо от менеджера
{
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  }
}

Дополнительные вопросы

  • Что такое workspaces (монорепозитории) и как их поддерживают npm, yarn и pnpm?
  • Почему lock-файл обязательно нужно коммитить в репозиторий?
  • Как pnpm экономит место именно на Windows, где хардлинки работают иначе, чем на Linux/macOS?