Pull to refresh

Что выбрать: Npm, Yarn или Pnpm?

Level of difficultyMedium
Reading time7 min
Views12K

На данный момент у нас используются три самых популярных менеджера пакетов (Npm, Yarn и Pnpm). И всё бы ничего, но разные команды начали периодически обращаться с проблемой несоответствия типов Typescript из наших транзитивных зависимостей. Выяснилось что это проблема Npm и Yarn, но как же её решать?

выглядит это примерно так, только при реэкспорте enum из library-f@1.0.0
по факту получаем enum из library-f@2.0.0

- library-a/
  - package.json
  - node_modules/
    - library-b/
      - package.json
      - node_modules/
        - library-f/
          - package.json  <-- library-f@1.0.0
    - library-c/
      - package.json
      - node_modules/
        - library-f/
          - package.json  <-- library-f@1.0.0
    - library-d/
      - package.json
    - library-e/
      - package.json
    - library-f/
      - package.json  <-- library-f@2.0.0

На ум сразу приходит самое очевидное решение: следить за версиями всех зависимостей в своих проектах и вовремя обновлять. К этому, естественно, необходимо стремиться всегда, но мы понимаем что на практике это крайне сложно, а в legacy-проектах или в проектах, у которых нет постоянной поддержки, и вовсе нереально. Следующим вариантом созрел Pnpm, тем более что в наших монорепах он себя уже продолжительное время отлично показывал. Я решил испытать его на действующих клиентских приложениях.

Предыстория

Когда я пришёл в компанию в конце 2018, то увидел что используется преимущественно Yarn. Тогда он применялся повсеместно, потому что работал гораздо быстрее Npm и имел интересные плюшки из коробки. Позже команды постепенно начали возвращаться на Npm, когда он подтянулся по скорости и от его использования перестали страдать.

Параллельно с этим у нас начали появляться монорепозитории на Rush для UI-kit'а, виджетов с обёртками и инструментов, на которые переходили с комбинации Lerna и Yarn. У нас была статья на тему ускорения сборки с помощью кеширования. Почему выбор пал на Rush, возможно, расскажу в одной из будущих статей. Сам инструмент позволяет использовать любой из трёх поддерживаемых менеджеров (Npm, Yarn и Pnpm), выбор за нами, но всё же его авторы в своих статьях намекают на преимущества Pnpm. Его мы и выбрали в монорепозиториях.

Исследование

Оговорюсь, что Yarn как целевое решение внутри компании уже не рассматривается, поэтому я сравнивал Pnpm исключительно с Npm. Помимо скорости, Yarn больше преимуществ для нас не имеет.

Детерминизм

Первым пунктом сразу обозначим разницу в установке зависимостей. Детерминизм означает, что при одной и той же операции должен возвращаться один и тот же результат.

У Npm структура модулей плоская: первая найденная версия каждой зависимости выносится на верхний уровень (это называется hoisting), благодаря чему в проекте можно использовать поднятые зависимости без их указания в package.json. Это так называемые фантомные зависимости, остальные версии остаются на своих местах. Какой здесь может быть недостаток? Представим ситуацию что первый разработчик установил зависимость B, которая зависит от C@^1.0.0. Зависимость C поднялась на верхний уровень с версией 1.0.1. Затем разработчик добавил зависимость A, которая зависит от C@1.0.0. У него всё будет хорошо, но если другой разработчик (или CI) установит зависимости из файла package-lock.json, то у него наверх поднимется зависимость C версии 1.0.0 из зависимости A, которая удовлетворяет B. И тогда зависимость B не получит патч-версию, которая могла быть для неё критична. Поэтому мы всегда должны помнить, что порядок установки зависимостей может влиять на конечные версии, и он может отличаться от того, что будет в CI.

А теперь разберём нашу ситуацию с типами, описанную в начале статьи. Возьмём наш пример с зависимостями и к пакету C добавим экспорт типов и enum'ов. В хост-проекте делаем import { ComponentB, MyEnum } from 'B';. Пока всё хорошо. Затем обновляем пакет B, в котором было мажорное обновление зависимости C, версия 2.0.0 и breaking change enum'а. В TS получаем ошибку несоответствия параметра, а в проекте без поддержки TS — ошибку в проде, потому что ComponentB ожидает enum из версии C@2.0.0, а получает из C@1.0.0. Можно нехитрыми манипуляциями добиться поднятия на верхний уровень node_modules более свежей версии (например, самостоятельно установить в корне эту зависимость нужной нам версии, у таких зависимостей приоритет). Но тогда может случиться такая проблема в зависимости A. Возможно, есть решения этой проблемы, если кто знает, как правильно разрешать типы и enum'ы в таких случаях, напишите в комментариях.

А что же Pnpm? Он в этом случае ведёт себя как npm@2: ничего не поднимает на верхний уровень, просто устанавливает по semver каждую версию зависимости, но не копирует, как это делал npm@2, а создаёт hardlink'и, поэтому node_modules не раздувается. Благодаря этому значительно быстрее устанавливаются зависимости, если, конечно, они у вас уже были, например при установке в других проектах.

Зависимости и дедупликация

Я предполагал, что с Pnpm зависимостей станет больше, или как минимум столько же. Оказалось, что практически во всех случаях их меньше, иногда столько же. Покажу на наглядном примере, для анализа я взял инструмент Statoscope.

npm
npm
Pnpm
Pnpm

Package.json был взят один и тот же, из одного из наших реальных проектов. Основной показатель, за который цепляется глаз — это Package copies: их стало более чем вдвое меньше. Это повлияло на общий вес проекта, даже на количество чанков. Я собрал проект и запустил в тестовом контуре, результат соответствует. Мало того, что мы стали увереннее в наших зависимостях, так бонусом получили ещё и небольшую оптимизацию по весу — мелочь, а приятно.

Почему у Npm с этим проблемы? Авторы Rush хорошо описали проблему дедупликации. Если вкратце, то Npm плохо дедуплицирует транзитивные зависимости, которые не поднимаются на верхний уровень. Отчасти это может быть связано с тем, что он не может дедуплицировать зависимости из разных веток графа.

Скорость

Не буду подробно сравнивать все сценарии, можно опираться на Pnpm. По моему субъективному ощущению, Pnpm в среднем быстрее в 1,5-3 раза. Для эксперимента возьму самый частый случай: сборку из lock-файла без node_modules и кеша. Каждый менеджер гонял по несколько раз, предварительно сбрасывая кеш, запускал командами npm ci и pnpm i --frozen-lockfile. Вот что получилось:

npm — 42–56 сек.
pnpm — 38–45 сек.

Разница не драматическая, к тому же Pnpm не поставляется из коробки, его нужно дополнительно ставить или встроить в базовый образ, для CI имеется ввиду. С кешем ситуация более ощутимая, нормально экономит время в повседневной разработке.

Результаты

Я рассказываю команде о Pnpm.
Я рассказываю команде о Pnpm.

По окончании исследования надо принести результат команде и подготовить ответы на два главных вопроса:

  1. Как мигрировать? С Npm на Pnpm переезжать максимально легко, Pnpm делает упор на то, что это тот же Npm, только быстрый. В большинстве случаев достаточно к Npm добавлять буковку p. Есть отличия в командах чистой установке (CI), очистке кеша, просмотре листа зависимостей и таких же мелочах, в остальном всё то же самое. К тому же Pnpm поддерживает команды из Yarn вроде add и remove, и можно выполнять скрипты без добавления слова run. Для нас это тоже хорошо, так как есть и Npm, и Yarn. От себя добавлю. что можно столкнуться с проблемами при переезде, но по моему опыту все они из‑за уже имеющихся проблем с зависимостями, которые просто не успели выстрелить, поэтому переезд на Pnpm поможет от них избавиться.

  2. Что нам даст переезд на Pnpm? Я для себя выделил несколько самых важных преимуществ:

    1. Консистентность. Один инструмент на все типы фронтенд-проектов в компании, от клиентских приложений до монореп. Это поможет улучшить DX, убрать зоопарк пакетных менеджеров, легче поддерживать CI и описывать документацию.

    2. Надёжность. Улучшение ситуации с зависимостями. Избавимся от магии с разрешением, сборки станут немного стабильнее, пропадут дубли, незначительно уменьшится вес вендоров, не будут приходить в поддержку с проблемами несоответствия зависимостей.

    3. Кеширование. Благодаря ссылкам и единому хранилищу мы можем без больших усилий ускорить сборку в CI-конвейерах. Авторы Pnpm в статье привели примеры из популярных CI-инструментов. Если ставить зависимости через pnpm install --prod, то можно сэкономить место в хранилище благодаря тому, что не будут устанавливаться dev-зависимости, нужные только на этапе разработки.

    4. Скорость.

    5. Фишки. Выделю strict‑peer‑dependencies, resolution‑mode, node‑linker и даже управление версиями Node.js. Pnpm интересный инструмент, он и про качество, и про возможности

Рекомендации

Дам свои рекомендации по работе с Pnpm:

  1. В package.json. Помогает в случаях, когда случайно вызвал Npm, особенно для новеньких.

    "scripts": {
      "preinstall": "npx only-allow pnpm", 
      ...
    }
  1. В package.json. Лучше всегда указывать актуальные версии Node.js и менеджера пакетов. Эта информация может сэкономить время тем, кто разворачивает у себя ваш проект.

    {
        "engines": {
            "node": "20",
            "pnpm": "8"
        }
    }

    .npmrc: engine-strict=true. При несоответствии версий установка будет завершаться с ошибкой

  2. В .npmrc : strict-peer-dependencies=true. Если будет несоответствие версий с peerDependencies, установка будет завершаться с ошибкой. Это гигиена, помогает не вляпываться в потенциальные проблемы. Rush тоже рекомендует включать эту настройку.

  3. В .npmrc: resolution-mode=time-based. Интересная опция, по умолчанию стоит highest. Pnpm рекомендует указывать значение time-based, я попробовал, но мне не понравилось. В случае packageA -> packageB сработало ограничение на packageB, но вот packageA -> packageB -> packageC — на packageC не сработало, и я получил две версии зависимости. Рассчитывал, что оно дедуплицируется в мою версию прямой зависимости, но нет. Возможно, просто баг.

  4. pnpm import — полезная команда для миграции для тех, кому очень важно не пересобирать lock-файл. Но если есть возможность, я рекомендую при миграции с других пакетных менеджеров удалить предыдущие lock-файлы и сгенерировать pnpm-lock заново.

Итоги

По моему мнению, Pnpm перекрывает все (по крайней мере, в нашей компании) возможные запросы. Можно организовать монорепы, он надёжней, в большинстве случаев быстрее. Он активно развивается и берёт из других инструментов то, что считает лучшим, например, тот же plug'n'play. Его смело можно брать за стандарт в компании, он стабильно набирает популярность последнее время.

Если у вас Npm или Yarn, то после прочтения этой статьи вы не начнёте внезапно страдать, и переход на Pnpm не будет означать, что можно перестать следить за зависимостями. Но в больших компаниях это немного упростит жизнь.

Вероятно, в будущем мы придём к ES-модулям и runtime-зависимостям, и нам не нужны будут пакетные менеджеры. Но я думаю, что не в ближайшие пару тройку лет. Что вы думаете по этому поводу, пишите в комментариях.

Only registered users can participate in poll. Log in, please.
Каков ваш выбор?
39.37% npm87
26.24% yarn58
27.6% pnpm61
6.79% свой вариант в комментариях15
221 users voted. 43 users abstained.
Tags:
Hubs:
Total votes 34: ↑34 and ↓0+34
Comments24

Articles

Information

Website
domclick.ru
Registered
Founded
Employees
501–1,000 employees
Location
Россия
Representative
Евгения Макарова