Вы когда-нибудь задумывались, что скрывается за пакетом 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, следующая:
npm → libnpmpack → pacote → tar → minizlib → zlib (Node.js)
Но цепочка зависимостей для распаковки (или «реификации», как ее называет npm), как в npm installor npm ci:
npm → @npmcli/arborist → pacote → tar → minizlib → zlib (Node.js)
Здесь много пакетов, которые необходимо обновить, но, к счастью, первые шаги уже сделаны. Поддержка Brotli была добавлена в minizlib 1.3.0 еще в сентябре 2019 года. Я разработал ее и внес поддержку Brotil в tar. Теперь это доступно в версии 6.2.0 . Понадобится какое-то время, но общий путь понятен.
Последняя проблема — обратная совместимость. Это не беспокоило Эвана Хана в первом RFC, поскольку Zopfli генерирует обратно совместимые файлы gzip. Однако Brotli — это совершенно новый формат сжатия, поэтому мне придется предложить очень осторожный план его внедрения. Я вижу следующий процесс:
-
Поддержка упаковки и распаковки добавлена в малый выпуск текущей версии npm.
Распаковка с помощью Brotli выполняется прозрачно.
-
Упаковка с использованием Brotli по умолчанию отключена и включается только в том случае, если выполняется одно из следующих условий:
Поле engines в package.json установлена на версию npm, поддерживающую Brotli.
Поле engines в package.json установлена версию ноды, которая включает версию npm, поддерживающую Brotli.
Поддержка Brotli явно включена в.npmrc
Упаковка с использованием 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)
SWATOPLUS
06.10.2023 08:05+1Сжатие npm на пакетов это хорошо. Но лучше из них выкинуть всякий мусор. Что бы остались только index.js и index.d.ts
Aquahawk
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.