[Перевод] Дорога в ад JavaScript-зависимостей

Каждый JavaScript-проект начинается с благих намерений, заключающихся в том, что его создатели обещают себе не использовать слишком много NPM-пакетов в ходе его разработки. Но даже если разработчики прилагают немалые усилия к тому, чтобы сдержать это обещание, NPM-пакеты постепенно проникают в их проекты. Размер файла package.json со временем растёт. А с package-lock.json после установки зависимостей происходит настоящий ужас, выражающийся в добавлениях и удалениях пакетов, особенно заметных при очередном PR…

wwhwqwn9q1x9j1aof1ks5pkdnk0.png

«Всё нормально», — говорит тимлид. Остальные члены команды согласно кивают. А что ещё делать-то? Нам всем хорошо от того, что экосистема JavaScript жива и здорова. Нам не надо каждый раз изобретать колесо и пытаться решать задачи, уже решённые сообществом опенсорса.

Предположим, вы собираетесь сделать блог и хотите воспользоваться Gatsby.js. Попытайтесь добавить этот генератор сайтов в зависимости своего проекта. А теперь — принимайте поздравления. Только что в вашем проекте оказалось 19000 дополнительных зависимостей. Это нормально? Насколько сложным может стать дерево JavaScript-зависимостей? Как дерево зависимостей превращается в ад? Давайте с этим разберёмся.

Что такое JavaScript-пакет?


В NPM (Node Package Manager, менеджер пакетов Node) хранится самый большой реестр пакетов в мире. Это — JavaScript-пакеты. NPM больше, чем RubyGems, PyPi и Maven вместе взятые. Этот вывод можно сделать на основе анализа данных проекта Module Counts, на котором отслеживается количество пакетов в популярных реестрах.

259d831584b0a9a5d1fd6226e3f799b0.png


Данные о количестве пакетов в популярных реестрах

Можно, подумать, что на этом графике представлены очень большие объёмы кода. Так оно и есть. Для того чтобы превратить некий проект в NPM-пакет, у этого пакета должен быть файл package.json. Такой пакет можно отправить в реестр NPM.

Что такое package.json?


Вот какие задачи решает package.json:

  • В нём перечислены пакеты, от которых зависит ваш проект (это — список зависимостей проекта).
  • В нём, с использованием правил семантического версионирования, задаются версии пакетов-зависимостей, которыми может пользоваться ваш проект.
  • Он позволяет воспроизвести среду, необходимую для работы пакета, и, в результате, упрощает передачу проекта другим разработчикам.


Файл package.json можно представить себе как файл README, накачанный стероидами. Здесь можно описать зависимости своего пакета, тут можно писать скрипты, выполняющиеся при сборке и тестировании проекта. В этом же файле находятся сведения о версии проекта, задаваемые его разработчиком, и описание проекта. Нас особенно интересует возможность package.json по указанию зависимостей проекта.

Пожалуй, то, что в этом файле указывают зависимости проекта, выглядит несколько настораживающе. Представьте, что имеется пакет, зависящий от другого пакета, а этот другой пакет зависит от ещё одного пакета. Такая цепочка зависимостей может быть сколько угодно длинной. Именно по этой причине установка единственного пакета, Gatsby.js, означает оснащение проекта 19000 дополнительных зависимостей.

Типы зависимостей в package.json


Для того чтобы лучше понять то, как со временем разрастаются списки зависимостей проектов, поговорим о разных типах зависимостей, которые может иметь проект. А именно, в package.json можно встретить следующие разделы, описывающие различные зависимости:

  • dependencies — это обычные зависимости, функционал которых используется в проекте, и к которым обращаются из его кода.
  • devDependencies — это зависимости разработки. Например — библиотека prettier, используемая для форматирования кода.
  • peerDependencies — если зависимости записываются в этот раздел, разработчик пакета тем самым сообщает тому, кто будет его устанавливать, о том, что ему понадобится конкретная версия указанного в этом разделе пакета.
  • optionalDependencies — тут перечисляют необязательные зависимости, такие, невозможность установки которых не нарушит процесс установки пакета.
  • bundledDependencies — тут даётся перечень зависимостей, которые встроены в пакет. Встраивание применяется для сторонних библиотек, которых нет в NPM, или в тех случаях, когда разработчик хочет включить в проект некоторые другие свои проекты в виде модулей.


Цель существования файла package-lock.json


Все мы знаем о том, что файл package-lock.json, в ходе работы над проектом, постоянно претерпевает изменения. Что-то из него удаляется, что-то в него добавляется. Особенно это заметно при просмотре PR, содержащих обновлённую версию этого файла. Мы часто принимаем это как данность. Файл package-lock.json автоматически генерируется каждый раз, когда меняется файл package.json или папка node_modules. Это позволяет поддерживать содержимое дерева зависимостей в точности таким, каким оно было при установке зависимостей проекта. Это позволяет, при установке проекта, воспроизвести дерево зависимостей. Это решает проблему наличия разных версий одного и того же пакета у разных разработчиков.

Рассмотрим проект, в составе зависимостей которого есть React. Соответствующая запись имеется в package.json. Если взглянуть в файл package-lock.json этого проекта, то там можно увидеть примерно следующее:

    "react": {
      "version": "16.13.0",
      "resolved": "https://registry.npmjs.org/react/-/react-16.13.0.tgz",
      "integrity": "sha512-TSavZz2iSLkq5/oiE7gnFzmURKZMltmi193rm5HEoUDAXpzT9Kzw6oNZnGoai/4+fUnm7FqS5dwgUL34TujcWQ==",
      "requires": {
        "loose-envify": "^1.1.0",
        "object-assign": "^4.1.1",
        "prop-types": "^15.6.2"
      }
    }


Файл package-lock.json — это большой список зависимостей проекта. Здесь перечислены версии зависимостей, пути (URI) к модулям, хеши, используемые для проверки целостности модуля и пакетов, нужных этому модулю. Если почитать этот файл, там можно найти записи обо всех пакетах, которые нужны React. Именно здесь и находится настоящий ад зависимостей. Здесь описано всё, в чём нуждается проект.

Разбираемся с зависимостями Gatsby.js


Как же получается так, что, установив всего одну зависимость, мы добавляем в проект целых 19000 зависимостей? Всё дело в зависимостях зависимостей. Именно поэтому у нас есть то, что есть:

$ npm install --save gatsby

...

+ gatsby@2.19.28
added 1 package from 1 contributor, removed 9 packages, updated 10 packages and audited 19001 packages in 40.382s


Если взглянуть в package.json, там можно найти лишь одну зависимость. Но если посмотреть на package-lock.json, то окажется, что перед нами — почти 14-килобайтный монстр. Более подробный ответ о том, что значат все те строки кода, которые попадают в package-lock.json, можно найти в файле package.json в репозитории Gatsby.js. Тут имеется очень много прямых зависимостей, а именно, по подсчётам npm, 132. Если у каждой из этих зависимостей будет хотя бы ещё одна зависимость, то общее количество зависимостей проекта удвоится — и у него окажется 264 зависимости. Конечно, в реальном мире всё не так. У каждой прямой зависимости проекта есть больше, чем 1 собственная зависимость. В результате список зависимостей проекта оказывается очень длинным.

Например — поинтересуемся тем, сколько раз в качестве зависимости для других пакетов используется библиотека lodash:

$ npm ls lodash
example-js-package@1.0.0
└─┬ gatsby@2.19.28
  ├─┬ @babel/core@7.8.6
  │ ├─┬ @babel/generator@7.8.6
  │ │ └── lodash@4.17.15  deduped
  │ ├─┬ @babel/types@7.8.6
  │ │ └── lodash@4.17.15  deduped
  │ └── lodash@4.17.15  deduped
  ├─┬ @babel/traverse@7.8.6
  │ └── lodash@4.17.15  deduped
  ├─┬ @typescript-eslint/parser@2.22.0
  │ └─┬ @typescript-eslint/typescript-estree@2.22.0
  │   └── lodash@4.17.15  deduped
  ├─┬ babel-preset-gatsby@0.2.29
  │ └─┬ @babel/preset-env@7.8.6
  │   ├─┬ @babel/plugin-transform-block-scoping@7.8.3
  │   │ └── lodash@4.17.15  deduped
  │   ├─┬ @babel/plugin-transform-classes@7.8.6
  │   │ └─┬ @babel/helper-define-map@7.8.3
  │   │   └── lodash@4.17.15  deduped
  │   ├─┬ @babel/plugin-transform-modules-amd@7.8.3
  │   │ └─┬ @babel/helper-module-transforms@7.8.6
  │   │   └── lodash@4.17.15  deduped
  │   └─┬ @babel/plugin-transform-sticky-regex@7.8.3
  │     └─┬ @babel/helper-regex@7.8.3
  │       └── lodash@4.17.15  deduped
  ...


К нашему счастью, большинство этих зависимостей представлены одной и той же версией lodash. А при таком подходе в node_modules будет лишь одна папка библиотеки lodash. Правда, обычно всё не совсем так. Иногда разным пакетам нужны разные версии одного и того же пакета. Именно поэтому и появилось много шуток об огромных размерах папки node_modules. В нашем случае, правда, всё не так уж и плохо:

$ du -sh node_modules
200M    node_modules


200 мегабайт — это не так уж и плохо. Я видел, как размер этой папки легко доходит до 700 Мб. Если вам интересно узнать о том, какие модули занимают больше всего места — можете выполнить следующую команду:

$ du -sh ./node_modules/* | sort -nr | grep '\dM.*'
 17M    ./node_modules/rxjs
8.4M    ./node_modules/@types
7.4M    ./node_modules/core-js
6.8M    ./node_modules/@babel
5.4M    ./node_modules/gatsby
5.2M    ./node_modules/eslint
4.8M    ./node_modules/lodash
3.6M    ./node_modules/graphql-compose
3.6M    ./node_modules/@typescript-eslint
3.5M    ./node_modules/webpack
3.4M    ./node_modules/moment
3.3M    ./node_modules/webpack-dev-server
3.2M    ./node_modules/caniuse-lite
3.1M    ./node_modules/graphql
...


Да, rxjs — коварный пакет.

Вот простая команда, которая помогает уменьшить размер папки node_modules и упростить её структуру:

$ npm dedup
moved 1 package and audited 18701 packages in 4.622s

51 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities


В ходе дедупликации npm пытается упростить структуру дерева зависимостей, находя зависимости, используемые другими зависимостями, и перемещая их так, чтобы организовать их совместное использование. Это относится к нашему примеру с lodash. Множество пакетов используют lodash @4.17.15, в результате для обеспечения их работоспособности достаточно установить эту версию библиотеки лишь один раз. Конечно, это — та ситуация, в которую мы попадаем с самого начала, только установив зависимости. Если в процессе работы над проектом в package.json добавляют новые зависимости, рекомендуется иногда вспоминать о команде npm dedup. Если вы используете менеджер пакетов yarn, там аналогичная команда выглядит как yarn dedupe. Но в ней, на самом деле, необходимости нет, так как оптимизация зависимостей выполняется автоматически, при выполнении команды yarn install.

Визуализация зависимостей


Интересовались графическим представлением зависимостей своего проекта? Если так — сформировать такое представление можно с помощью специальных инструментов. Рассмотрим некоторые из них.

Ниже показан результат визуализации зависимостей, полученный с помощью npm.anvaka.com/.

dfbf5d37377bd62f4ea5cf422de62c74.png


Визуализация зависимостей, выполняемая с помощью npm.anvaka.com

Тут можно видеть взаимосвязи пакетов-зависимостей проекта Gatsby.js. Результат получается похожим на огромную паутину. У проекта Gatsby.js так много зависимостей, что эта «паутина» чуть не «подвесила» мой браузер. Вот, если интересно, ссылка на эту схему. Она может быть представлена и в 3D-виде.

Вот визуализация, сделанная с помощью npm.broofa.com.

285cd6c52a00d9e87a7bc2095df6023f.png


Фрагмент визуализации зависимостей, выполненной с помощью npm.broofa.com

Это похоже на блок-схему. Она, для Gatsby.js, получается очень сложной. Взглянуть на неё можно здесь. Элементы схемы можно раскрасить, основываясь на оценках с npms.io. На сайт можно загрузить собственный файл package.json.

Средство Package Phobia позволяет, до установки пакета, узнать о том, сколько места ему требуется.

25cd26ae4fc708f0c6900c5ad3a5dead.png


Сведения о пакете, полученные с помощью Package Phobia

Здесь можно узнать о размере опубликованного пакета, и о том, какое место на диске он займёт после установки.

Итоги: с большой силой приходит большая ответственность


В итоге хочется сказать, что JavaScript и NPM — это замечательные инструменты. Очень хорошо то, что у современных разработчиков есть возможность пользоваться огромным набором зависимостей. Выполнить команду npm install для того чтобы избавить себя от написания пары строчек кода — это так легко, что иногда мы забываем о последствиях.

Сейчас, когда вы дочитали до этого места, у вас должно сложиться более полное понимание особенностей устройства деревьев зависимостей npm-проектов. Если вы добавляете в проект библиотеку, которая очень велика, или если вы просто исследуете зависимости своего проекта, вы всегда можете воспользоваться тем, что мы тут обсудили, и проанализировать зависимости.

Уважаемые читатели! Стремитесь ли вы к тому, чтобы пользоваться как можно меньшим количеством зависимостей в своих npm-проектах?

a_bsaactpbr8fltzymtkhqbw1d4.png

© Habrahabr.ru