По умолчанию npm публикует в registry весь модуль целиком. За исключением явно указанных в .gitignore файлов. Это отбрасывает зависимости, но все равно позволяет куче не очень нужных файлов просочиться в опубликованное. После чего благодарные пользователи ждут, пока все это скачается. Для grunt, кстати, ждать придется порядка 6 мегабайт. А он такой обычно не один.

Я решил разобраться, как измерить размер своих модулей после публикации и, по возможности, этот размер уменьшить. В качестве примера буду использовать модуль check-more-types, который содержит всего несколько файлов. Плюс юнит тесты и документацию, которая собирается в README markdown файл.


В первую очередь мы должны посчитать текущий размер модуля. NPM хранит все файлы в виде tar архивов, так что достаточно будет создать такой архив и посмотреть его размер. Более того, у npm есть для этого специальная команда npm pack, создающая архив из содержимого указанной директории. Mathias Bynens предлагает следующий скрипт для определения размера модуля:

tarball="$(npm pack .)"; wc -c "${tarball}"; tar tvf "${tarball}"; rm "${tarball}";


Я измерил размер архива для коммита с хешом 3ada360:

немного shell магии
$ tarball="$(npm pack .)"; wc -c "${tarball}"; tar tvf "${tarball}"; rm "${tarball}";

   25184 check-more-types-2.1.2.tgz
-rw-r--r--  0 501    20       1977 Nov 19 13:55 package/package.json
-rw-r--r--  0 501    20         64 Nov 19 13:18 package/.npmignore
-rw-r--r--  0 501    20      19703 Nov 19 13:49 package/README.md
-rw-r--r--  0 501    20       1073 Nov 19 13:18 package/LICENSE
-rw-r--r--  0 501    20       2534 Nov 19 13:18 package/Gruntfile.js
-rw-r--r--  0 501    20      18204 Nov 19 13:18 package/check-more-types.js
-rw-r--r--  0 501    20       6723 Nov 19 13:49 package/check-more-types.min.js
-rw-r--r--  0 501    20        600 Nov 19 13:49 package/bower.json
-rw-r--r--  0 501    20        162 Nov 19 13:18 package/.travis.yml
-rw-r--r--  0 501    20       1756 Nov 19 13:18 package/.jshintrc
-rw-r--r--  0 501    20        655 Nov 19 13:18 package/docs/README.tmpl.md
-rw-r--r--  0 501    20       1936 Nov 19 13:18 package/docs/badges.md
-rw-r--r--  0 501    20        255 Nov 19 13:18 package/docs/footer.md
-rw-r--r--  0 501    20        240 Nov 19 13:18 package/docs/install.md
-rw-r--r--  0 501    20      13707 Nov 19 13:49 package/docs/use.md
-rw-r--r--  0 501    20        127 Nov 19 13:18 package/test/check-more-types-minified-spec.js
-rw-r--r--  0 501    20         78 Nov 19 13:18 package/test/check-more-types-spec.js
-rw-r--r--  0 501    20        467 Nov 19 13:18 package/test/load-under-node-test.js
-rw-r--r--  0 501    20        738 Nov 19 13:18 package/test/synthetic-browser-spec.js
-rw-r--r--  0 501    20      37754 Nov 19 13:18 package/test/unit-tests.js



Что мы видим? Куча файлов и размер сжатых данных в 251184 байт. Для начала немного автоматизируем этот процесс. Я нашел хорошую утилиту для получения размера без использования шелл команд: pkgfiles за авторством Tim Oxley.

Я инсталлировал утилиту как дев зависимость и добавил ее в “prepublish” скрипт для package.json:

{
  "devDependencies": {
    "pkgfiles": "2.3.0"
  },
  "scripts": {
    "prepublish": "pkgfiles"
  }
}


“Prepublish” скрипт будет выполняться каждый раз при локальной установке и по команде npm publish. Посмотрим на результат npm run publish после наших изменений:

результат npm run prepublish
$ npm run prepublish

> check-more-types@2.1.2 prepublish /Users/kensho/git/check-more-types
> pkgfiles


PATH                                    SIZE       %
.npmignore                              0 B        0%
test/check-more-types-spec.js           78 B       0%
test/check-more-types-minified-spec.js  127 B      0%
.travis.yml                             162 B      0%
docs/install.md                         240 B      0%
docs/footer.md                          255 B      0%
test/load-under-node-test.js            467 B      0%
bower.json                              600 B      1%
docs/README.tmpl.md                     655 B      1%
test/synthetic-browser-spec.js          738 B      1%
LICENSE                                 1.07 kB    1%
.jshintrc                               1.76 kB    2%
docs/badges.md                          1.94 kB    2%
package.json                            2.05 kB    2%
Gruntfile.js                            2.53 kB    2%
check-more-types.min.js                 6.72 kB    6%
docs/use.md                             13.71 kB   13%
check-more-types.js                     18.2 kB    17%
README.md                               19.7 kB    18%
test/unit-tests.js                      37.75 kB   35%

DIR                                     SIZE       %
docs/                                   16.79 kB   15%
test/                                   39.16 kB   36%
.                                       108.77 kB  100%

PKGFILES SUMMARY
Size on Disk with Dependencies  ~126.72 MB
Size with Dependencies          ~88.58 MB
Publishable Size                ~108.77 kB
Number of Directories           3
Number of Files                 20



Очень детальная информация. И самые “тяжелые” файлы указаны последними, что дает возможность удобно анализировать результаты в терминале. Больше всего места занимают директории с документацией и тестами — и это при том, что мы не собираемся их публиковать!

Есть три способа указать, какие файлы не будут публиковаться в npm. Мы используем способ по умолчанию: файлы, указанные в .gitignore, автоматически заносятся в черный список. И мы можем создать еще один файл, .npmignore, в котором указать независящий от git набор файлов, который мы не хотим публиковать. Альтернативные способ: добавить файлы в “белый список” с помощью package.json. Лично я предпочитаю именно такой способ. Обратите внимание, что ряд файлов, такие как package.json или README, автоматически находятся в белом списке.

{
  "files": [
    "bower.json",
    "check-more-types.js",
    "check-more-types.min.js"
  ]
}


А чтобы исключить файлы из уже добавленных, можно воспользоваться восклицательным знаком. Например, если в вашей директории src, которую вы хотите публиковать, есть поддиректория test, которую вы публиковать совсем не хотите, то:

{
  "files": [
    "src",
    "!src/test"
  ]
}


Ну а если в одной директории src у вас случились файлы как для production, так и для test/staging, то вы можете исключить файлы по одному или группами:

{
  "files": [
    "src/*.js",
    "!src/*-spec.js"
  ]
}


Хеш коммита со всеми этими изменениями начинается с bc3e2a1. Посмотрим, что получилось с размером публикуемого модуля:

еще один результат npm run prepublish
$ npm run prepublish

> check-more-types@2.1.2 prepublish /Users/kensho/git/check-more-types
> pkgfiles


PATH                     SIZE      %
bower.json               600 B     1%
LICENSE                  1.07 kB   2%
package.json             2.15 kB   4%
check-more-types.min.js  6.72 kB   14%
check-more-types.js      18.2 kB   38%
README.md                19.7 kB   41%

DIR                      SIZE      %
.                        48.45 kB  100%

PKGFILES SUMMARY
Size on Disk with Dependencies  ~126.72 MB
Size with Dependencies          ~88.58 MB
Publishable Size                ~48.45 kB
Number of Directories           1
Number of Files                 6



Получилось все неплохо: размер публикуемого модуля уменьшался на 55% со 107 килобайт до 48. Это общее уменьшение размера, но мы еще можем посмотреть, что именно поменялось внутри tar архива. К сожалению, npm pack вызывает prepublish скрипт и не может корректно обработать его вывод. Поэтому я временно переименую prepublish и добавлю “tarball=...” под именем reuse:

результат npm run size
$ npm run size

> check-more-types@2.1.2 size /Users/kensho/git/check-more-types
> tarball="$(npm pack .)"; wc -c "${tarball}"; tar tvf "${tarball}"; rm "${tarball}";

   13179 check-more-types-2.1.2.tgz
-rw-r--r--  0 501    20       2256 Nov 19 14:09 package/package.json
-rw-r--r--  0 501    20      19703 Nov 19 13:58 package/README.md
-rw-r--r--  0 501    20       1073 Nov 19 13:18 package/LICENSE
-rw-r--r--  0 501    20      18204 Nov 19 13:58 package/check-more-types.js
-rw-r--r--  0 501    20       6723 Nov 19 13:58 package/check-more-types.min.js
-rw-r--r--  0 501    20        600 Nov 19 13:58 package/bower.json



Теперь клиенту нужно сказать только 13 килобайт вместо 28, а это 50% уменьшение размера!

Мне также пришла в голову идея показывать размер публикуемого модуля при каждом push из локального репозитория в “remote master”. Чтобы это сделать, достаточно добавить обе команды, size и pkgfiles, в pre-push шаг и воспользоваться модулем “pre-git”:

npm install -D pre-git

package.json
{
  "scripts": {
    "pkgfiles": "pkgfiles",
    "size": "tarball=\"$(npm pack .)\"; wc -c \"${tarball}\"; tar tvf \"${tarball}\"; rm \"${tarball}\";"
  },
  "config": {
    "pre-git": {
      "pre-push": [
        "npm run size",
        "npm run pkgfiles"
      ]
    }
  }
}
</spoiler>


Чтобы проверить, что все сработало, я увеличил версию пакета с 2.1.2 до 2.2.0 и воспользовался для установки чистой директорий и npm версии “3.4.0”:

$ time npm i check-more-types@2.1.2
/private/tmp/test-small
L-- check-more-types@2.1.2
real  0m2.706s
user  0m1.419s
sys 0m0.323s


Почти 3 секунды. Сотрем директорию node_modules и попробуем новую версию пакета:

$ rm -rf node_modules/
$ time npm i check-more-types@2.2.0
/private/tmp/test-small
L-- check-more-types@2.2.0
real  0m1.716s
user  0m1.244s
sys 0m0.198s


Мы успешно откусили 1 секунду от инсталляции — а это 30% от всего времени инсталяции для нашего маленького модуля!

Полная версия использованного мной для “очистки” package.json (можно посмотреть в моем репозитории):

npm install -D pkgfiles pre-git

package.json целиком
{
  "devDependencies": {
    "pkgfiles": "2.3.0",
    "pre-git": "1.3.0"
  },
  "scripts": {
    "pkgfiles": "pkgfiles",
    "size": "tarball=\"$(npm pack .)\"; wc -c \"${tarball}\"; tar tvf \"${tarball}\"; rm \"${tarball}\";"
  },
  "config": {
    "pre-git": {
      "pre-push": [
        "npm run size",
        "npm run pkgfiles"
      ]
    }
  }
}



Кстати, эту версию можно немного уменьшить, если использовать “t” вместо “tarball”:

"scripts": {
  "pkgfiles": "pkgfiles",
  "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";"
}
Поделиться с друзьями
-->

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


  1. gearbox
    16.05.2016 17:01
    +2

    Унес в закладки, спасибо! Удивляет реакция аудитории — видимо в шоке от возвращения мегамозга. Статья реально полезная, даже если для кого то и покажется очевидой.


  1. Mithgol
    17.05.2016 01:24
    +5

    Читателей этой блогозаписи хочется лишний раз предупредить, что файл .npmignore используется утилитою npm не как дополнение к .gitignore, а как замена. Так что создавать .npmignore лучше всего копированием .gitignore с последующим доредактированием, а не то можно обнаружить в пакете много лишнего.


    1. miripiruni
      17.05.2016 13:25
      +3

      Гораздо удобнее использовать поле files в package.json. Это white list того, что должно быть в пакете. Случайные залетные файлы туда не смогут попасть просто потому что вы забыли их внести в black list в .npmignore.


      1. Mithgol
        17.05.2016 21:29

        Мне психологически проще работать в таких обстоятельствах, когда приходится заносить ненужные файлы в black list, а не в таких обстоятельствах, когда приходится заносить нужные файлы в white list.

        Почему проще?

        Потому, что так меньше цена возможной ошибки.

        Ведь если я позабуду занести ненужный файл в black list, то пакет всего лишь напрасно распухнет в объёме; но если я позабуду занести нужный файл в white list, то пакет вообще не будет корректно работать.


  1. tenbits
    17.05.2016 19:10
    +1

    Именно, но к сожалению, уж очень много модулей в npm страдают этой болезнью. Мы например, релизим модули скриптом через отдельный бранч. Грубо говоря — создаём ветку release; удаляем из git-индекса всё; генерируем .gitignore с *(ignore all), там же добавляем исключения для релизных файлов, часто это лишь lib/**, readme.md, package.json, bower.json; дибавляем всё в гит; пушим в ветку release; создаем тэг. Таким образом у нас и npm чист, и bower пакет чист, а также все git releases.


    Вот перечень команд:
    [
        `npm run bump`,
        `git commit -a -m "v${version}"`,
        `git push origin master`,
    
        `git checkout -B release`,
        `npm run build`,
        `npm run ignorefiles`,
        `npm publish`,  
        `git rm -r --cached .`,
        `git add -A`,
        `git commit -a -m "v${version}"`,
        `git push origin release -ff`,
        `git tag v${version}`,
        `git push --tags`,
        `git checkout master -ff`
    ]


  1. Radiocity
    24.05.2016 18:36

    в package.json лишний закрывающий тег спойлера


  1. bustEXZ
    24.05.2016 18:37

    Поправь последний спойлер


  1. fr_ant
    24.05.2016 18:37

    Совсем недавно негодовал по этому поводу. Данную статью нужно читать всем кто хоть как-то связан с npm/node, а еще было-бы хорош видеть её на англ. языке.