Вы когда-нибудь задумывались, что скрывается за пакетом npm?

По сути, это не что иное, как сжатый gzip'ом архив. При разработке программного обеспечения исходный код почти всегда поставляется в виде файлов .tar.gz или .tgz. Сжатие gzip поддерживается каждым HTTP-сервером и веб-браузером. Но вот что самое интересное: gzip начинает устаревать, уступая место новым алгоритмам сжатия вроде Brotli и ZStandard. Теперь представьте себе мир, в котором npm использует один из этих новых алгоритмов. В этом посте я углублюсь в вопросы сжатия и исследую возможности модернизации стратегии сжатия npm.

Что насчёт конкуренции?

Двумя основными игроками в этой сфере являются Brotli и ZStandard (сокращённо zstd). Brotli был выпущен Google в 2013 году, а zstd — запрещённой соцсетью на букву F в 2016 году. С тех пор они были стандартизированы в RFC 7932 и RFC 8478 соответственно и получили широкое распространение во всей индустрии программного обеспечения.

На самом деле именно объявление Arch Linux о том, что они собираются сжимать свои пакеты с помощью zstd по умолчанию, заставило меня призадуматься. Arch Linux ни в коем случае не был первым и тем более единственным проектом. Но чтобы выяснить, имеет ли это смысл для экосистемы Node, нужно провести несколько тестов. А это значит, что нужно пробиться через tar.

Сравнительный анализ, часть 1

Я собираюсь запустить tar и посмотреть, какие параметры для сравнения я могу получить, переключаясь gzip, Brotli и zstd. Я протестирую сам пакет npm, так как он довольно популярен, в среднем его загружают более 4 миллионов раз в неделю, а ещё он большой: около 11 МБ в распакованном виде.

$ curl --remote-name https://registry.npmjs.org/npm/-/npm-9.7.1.tgz
$ ls -l --human npm-9.7.1.tgz 
-rw-r--r-- 1 jamie users 2.6M Jun 16 20:30 npm-9.7.1.tgz 
$ tar --extract --gzip --file npm-9.7.1.tgz
$ du --summarize --human --apparent-size package
11M	package

gzip сразу даёт хорошие результаты, сжимая с 11 МБ до 2,6 МБ со степенью сжатия около 0,24. Но на что способны  конкуренты? На данный момент я буду придерживаться параметров по умолчанию:

$ brotli --version
brotli 1.0.9
$ tar --use-compress-program brotli --create --file npm-9.7.1.tar.br package
$ zstd --version
*** Zstandard CLI (64-bit) v1.5.5, by Yann Collet ***
$ tar --use-compress-program zstd --create --file npm-9.7.1.tar.zst package
$ ls -l --human npm-9.7.1.tgz npm-9.7.1.tar.br npm-9.7.1.tar.zst 
-rw-r--r-- 1 jamie users 1.6M Jun 16 21:14 npm-9.7.1.tar.br
-rw-r--r-- 1 jamie users 2.3M Jun 16 21:14 npm-9.7.1.tar.zst
-rw-r--r-- 1 jamie users 2.6M Jun 16 20:30 npm-9.7.1.tgz 

Ух ты! Без конфигурирования и Brotli, и zstd обходят gzip, но Brotli явный победитель. Он обеспечивает степень сжатия 0,15, в то время как у zstd только 0,21. В реальном выражении это означает экономию около 1 МБ. Вроде пустяковая разница, но при 4 миллионах загрузок в неделю это даёт экономию 4 ТБ в пропускной способности в неделю.

Сравнительный анализ, часть 2: Электрическое бугалу

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

Чтобы проверить это, я собираюсь использовать Hyperfine — инструмент для сравнительного анализа командной строки. Распаковка каждого из пакетов, которые я ранее создавал 100 раз, должна дать мне хорошее представление об относительной скорости распаковки.

$ hyperfine --runs 100 --export-markdown hyperfine.md \
  'tar --use-compress-program brotli --extract --file npm-9.7.1.tar.br --overwrite' \
  'tar --use-compress-program zstd --extract --file npm-9.7.1.tar.zst --overwrite' \
  'tar --use-compress-program gzip --extract --file npm-9.7.1.tgz --overwrite'

Команда

Средний [мс]

Минимальный [мс]

Максимальный [мс]

Относительный

tar –use-compress-program brotli –extract –file npm-9.7.1.tar.br –overwrite

51,6 ± 3,0

47,9

57,3

1,31 ± 0,12

tar –use-compress-program zstd –extract –file npm-9.7.1.tar.zst –overwrite

39,5 ± 3,0

33,5

51,8

1.00

tar –use-compress-program gzip –extract –file npm-9.7.1.tgz –overwrite

47,0 ± 1,7

44,0

54,9

1,19 ± 0,10

На этот раз вперёд вырывается zstd, за ним следуют gzip и Brotli. Это логично, поскольку «сжатие в реальном времени» — одна из главных функций, рекламируемых в документации zstd. Хотя Brotli на 31% медленнее по сравнению с zstd, в реальном выражении это всего лишь 12 мс. И по сравнению с gzip он медленнее всего на 5 мс.

На практике это означает, что вам понадобится соединение со скоростью более 1 Гбит/с, чтобы компенсировать потерю 5 мс при распаковке по сравнению с 1 МБ, который будет сэкономлен за счёт размера пакета.

Сравнительный анализ, часть 3. На этот раз все серьёзно

До сих пор я брал настройки Brotli и zstd по умолчанию, но у обоих есть множество крутилок и регуляторов, которые вы можете настроить, чтобы изменить степень сжатия, а также скорость сжатия или распаковки. Здесь мне помог отраслевой стандарт lzbench. Он может протестировать каждый архиватор и в конце выдать красивую таблицу со всеми данными.

Здесь я должен вас предостеречь от нескольких вещей. Во‑первых, lzbench не может сжимать весь каталог, например tar, поэтому для этого теста я решил использовать lib/npm.js. Во‑вторых, в состав lzbench не входит инструмент gzip. Вместо этого он использует zlib, базовую библиотеку gzip. И, в‑третьих, версии каждого архиватора не совсем актуальны. Последняя актуальная версия zstd — 1.5.5, выпущенная 4 апреля 2023 г., тогда как lzbench использует версию 1.4.5, выпущенную 22 мая 2020 г. Последняя версия Brotli — 1.0.9, выпущенная 27 августа 2020 г., тогда как lzbench использует версию, выпущенную 1 октября 2019 года.

$ lzbench -o1 -ezlib/zstd/brotli package/lib/npm.js

Архиватор

Сжатие

Распаковка

Размер файла

Соотношение

Имя файла

memcpy

117330 MB/s

121675 MB/s

13141

100.00

package/lib/npm.js

zlib 1.2.11 -1

332 MB/s

950 MB/s

5000

38.05

package/lib/npm.js

zlib 1.2.11 -2

382 MB/s

965 MB/s

4876

37.11

package/lib/npm.js

zlib 1.2.11 -3

304 MB/s

986 MB/s

4774

36.33

package/lib/npm.js

zlib 1.2.11 -4

270 MB/s

1009 MB/s

4539

34.54

package/lib/npm.js

zlib 1.2.11 -5

204 MB/s

982 MB/s

4452

33.88

package/lib/npm.js

zlib 1.2.11 -6

150 MB/s

983 MB/s

4425

33.67

package/lib/npm.js

zlib 1.2.11 -7

125 MB/s

983 MB/s

4421

33.64

package/lib/npm.js

zlib 1.2.11 -8

92 MB/s

989 MB/s

4419

33.63

package/lib/npm.js

zlib 1.2.11 -9

95 MB/s

986 MB/s

4419

33.63

package/lib/npm.js

zstd 1.4.5 -1

594 MB/s

1619 MB/s

4793

36.47

package/lib/npm.js

zstd 1.4.5 -2

556 MB/s

1423 MB/s

4881

37.14

package/lib/npm.js

zstd 1.4.5 -3

510 MB/s

1560 MB/s

4686

35.66

package/lib/npm.js

zstd 1.4.5 -4

338 MB/s

1584 MB/s

4510

34.32

package/lib/npm.js

zstd 1.4.5 -5

275 MB/s

1647 MB/s

4455

33.90

package/lib/npm.js

zstd 1.4.5 -6

216 MB/s

1656 MB/s

4439

33.78

package/lib/npm.js

zstd 1.4.5 -7

140 MB/s

1665 MB/s

4422

33.65

package/lib/npm.js

zstd 1.4.5 -8

101 MB/s

1714 MB/s

4416

33.60

package/lib/npm.js

zstd 1.4.5 -9

97 MB/s

1673 MB/s

4410

33.56

package/lib/npm.js

zstd 1.4.5 -10

97 MB/s

1672 MB/s

4410

33.56

package/lib/npm.js

zstd 1.4.5 -11

37 MB/s

1665 MB/s

4371

33.26

package/lib/npm.js

zstd 1.4.5 -12

27 MB/s

1637 MB/s

4336

33.00

package/lib/npm.js

zstd 1.4.5 -13

20 MB/s

1601 MB/s

4310

32.80

package/lib/npm.js

zstd 1.4.5 -14

18 MB/s

1582 MB/s

4309

32.79

package/lib/npm.js

zstd 1.4.5 -15

18 MB/s

1582 MB/s

4309

32.79

package/lib/npm.js

zstd 1.4.5 -16

9.03 MB/s

1556 MB/s

4305

32.76

package/lib/npm.js

zstd 1.4.5 -17

8.86 MB/s

1559 MB/s

4305

32.76

package/lib/npm.js

zstd 1.4.5 -18

8.86 MB/s

1558 MB/s

4305

32.76

package/lib/npm.js

zstd 1.4.5 -19

8.86 MB/s

1559 MB/s

4305

32.76

package/lib/npm.js

zstd 1.4.5 -20

8.85 MB/s

1558 MB/s

4305

32.76

package/lib/npm.js

zstd 1.4.5 -21

8.86 MB/s

1559 MB/s

4305

32.76

package/lib/npm.js

zstd 1.4.5 -22

8.86 MB/s

1589 MB/s

4305

32.76

package/lib/npm.js

brotli 2019-10-01 -0

604 MB/s

813 MB/s

5182

39.43

package/lib/npm.js

brotli 2019-10-01 -1

445 MB/s

775 MB/s

5148

39.18

package/lib/npm.js

brotli 2019-10-01 -2

347 MB/s

947 MB/s

4727

35.97

package/lib/npm.js

brotli 2019-10-01 -3

266 MB/s

936 MB/s

4645

35.35

package/lib/npm.js

brotli 2019-10-01 -4

164 MB/s

930 MB/s

4559

34.69

package/lib/npm.js

brotli 2019-10-01 -5

135 MB/s

944 MB/s

4276

32.54

package/lib/npm.js

brotli 2019-10-01 -6

129 MB/s

949 MB/s

4257

32.39

package/lib/npm.js

brotli 2019-10-01 -7

103 MB/s

953 MB/s

4244

32.30

package/lib/npm.js

brotli 2019-10-01 -8

84 MB/s

919 MB/s

4240

32.27

package/lib/npm.js

brotli 2019-10-01 -9

7.74 MB/s

958 MB/s

4237

32.24

package/lib/npm.js

brotli 2019-10-01 -10

4.35 MB/s

690 MB/s

3916

29.80

package/lib/npm.js

brotli 2019-10-01 -11

1.59 MB/s

761 MB/s

3808

28.98

package/lib/npm.js

Это в значительной степени подтверждает то, что я показал ранее. zstd способен обеспечить более высокую скорость распаковки, чем gzip или Brotli, а также немного превосходит gzip по степени сжатия. Brotli, с другой стороны, имеет сопоставимую скорость распаковки и сопоставимую степень сжатия с gzip на низких уровнях качества, но на уровнях 10 и 11 он способен дать более высокую степень сжатия, чем gzip и zstd.

Всё уже было в Симпсонах

Теперь, когда я закончил тестирование производительности, нужно вернуться назад и взглянуть на мою первоначальную идею о замене gzip на другой стандарт сжатия npm. Как выяснилось, у Эвана Хана в 2022 году возникла аналогичная идея, которую он изложил на собрании npm RFC. Он предложил использовать Zopfli, обратно совместимую библиотеку сжатия gzip и старшего (и более крутого ????) брата Brotli. Zopfli способен создавать архивы меньшего размера, но за счёт сильного снижения скорости сжатия.

Теоретически это лёгкая победа для экосистемы npm. А если вы посмотрите запись заседания RFC или прочитаете протоколы собрания, то заметите, что практически все поддерживают это предложение. Однако есть одно большое препятствие, которое не позволяет немедленно принять решение с RFC и, в конечном итоге, приводит к отказу от идеи. Проблема в отсутствии встроенной реализации JavaScript.

Итак, у меня есть результаты этого более раннего RFC, собственные результаты сравнительного анализа Brotli и zstd. Что же нужно сделать, чтобы создать собственный сильный RFC?

Собираем всё вместе

Эталонные реализации Brotli и zstd написаны на C. И хотя в реестре npm имеется множество портов с использованием Emscripten или WASM, у Brotli есть реализация в модуле zlib Node.js, и она существует, начиная с Node.js 10.16.0, выпущенном в мае 2019 года. Я открыл раздел «Проблемы» в репозитории Node.js на GitHub, чтобы добавить поддержку zstd, но для того, чтобы попасть в LTS‑версию, потребуется много времени, не говоря уже об остальной части цепочки зависимостей npm. Я и без того уже склонялся к Brotli, но это лишь укрепило мою уверенность.

Выбор алгоритма — это одно, а реализация — другое. Текущая поддержка сжатия gzip в npm в конечном итоге исходит от самого Node.js. Но цепочка зависимостей между npm и Node.js длинная и немного различается в зависимости от того, упаковываете вы пакет или распаковываете.

Цепочка зависимостей для упаковки, как в npm packor npm publish, следующая:

npmlibnpmpackpacotetarminizlibzlib (Node.js)

Но цепочка зависимостей для распаковки (или «реификации», как ее называет npm), как в npm installor npm ci:

npm@npmcli/arboristpacotetarminizlibzlib (Node.js)

Здесь много пакетов, которые необходимо обновить, но, к счастью, первые шаги уже сделаны. Поддержка Brotli была добавлена ​​в minizlib 1.3.0 еще в сентябре 2019 года. Я разработал ее и внес поддержку Brotil в tar. Теперь это доступно в версии 6.2.0 . Понадобится какое-то время, но общий путь понятен.

Последняя проблема — обратная совместимость. Это не беспокоило Эвана Хана в первом RFC, поскольку Zopfli генерирует обратно совместимые файлы gzip. Однако Brotli — это совершенно новый формат сжатия, поэтому мне придется предложить очень осторожный план его внедрения. Я вижу следующий процесс:

  1. Поддержка упаковки и распаковки добавлена ​​в малый выпуск текущей версии npm.

    1. Распаковка с помощью Brotli выполняется прозрачно.

    2. Упаковка с использованием Brotli по умолчанию отключена и включается только в том случае, если выполняется одно из следующих условий:

      1. Поле engines в package.json установлена на версию npm, поддерживающую Brotli.

      2. Поле engines в package.json установлена версию ноды, которая включает версию npm, поддерживающую Brotli.

      3. Поддержка Brotli явно включена в.npmrc

  1. Упаковка с использованием Brotli включена по умолчанию в следующем основном выпуске npm после того, как LTS-версию Node.js, в которую он входит, перестали поддерживать.

Допустим, Node.js 22 поставляется с npm 10, в котором есть поддержка Brotli. Node.js 22 перестанет получать обновления LTS в апреле 2027 года. Тогда следующая основная версия npm после этой даты должна включить упаковку Brotli по умолчанию.

Я признаю, что это невероятно долгий переходный период. Однако это гарантирует, что если вы используете версию Node.js, которая все ещё поддерживается, на вас это не окажет видимого влияния. И это по-прежнему позволяет ранним пользователям подписаться на поддержку Brotli. 

Что дальше?

Я должен признать, что моё путешествие по исследованию сжатия npm только начинается. Нужно сделать ещё много шагов. Прежде всего, провести более обширное сравнение с 250 наиболее загружаемыми пакетами npm, вместо того, чтобы сосредотачиваться на одном пакете. Когда все будет готово, мне нужно составить проект npm RFC и получить отзывы от более широкого сообщества.

Если у вас тоже есть, что сказать на эту тему, автор приглашает к обсуждению в Mastodon  @JamieMagee@infosec.exchange, или в Twitter @Jamie_Magee.

Комментарии (2)


  1. Aquahawk
    06.10.2023 08:05
    +6

    Zopfli тормозное говнище дающее крайне малый выигрыш, а zstd намного более гибок чем всё остальное. Шикарные графики есть тут: https://community.centminmod.com/threads/round-4-compression-comparison-benchmarks-zstd-vs-brotli-vs-pigz-vs-bzip2-vs-xz-etc.18669/

    По сути график показывает что для любого конфига brotli, zstd может предложить либо большее сжатие за то же время, либо меньшее время для того же уровня сжатия. D сдшслрщгыу zstd показывает себя просто отлично. Только монополия гугла в вебе мешает тому чтобы zstd появилось как сжатие протокольного уровня в http.


  1. SWATOPLUS
    06.10.2023 08:05
    +1

    Сжатие npm на пакетов это хорошо. Но лучше из них выкинуть всякий мусор. Что бы остались только index.js и index.d.ts