Наши кодовые базы растут, и вынос кода в npm-пакеты — один из самых простых и рабочих способов держать этот рост под контролем. Фронтендеры уже освоились со сборкой приложений — мы минифицируем и бандлим код для ускорения загрузки, подключаем полифиллы и транспилируем для поддержки старых браузеров. Есть соблазн для библиотек просто делать все то же самое — но это ошибка, потому что у библиотек совсем другие ограничения. Вот мой топ (нефункциональных) ценностей библиотеки:
-
Работает на всех целевых платформах.
-
Подключается без специальных приседаний.
-
Не мешает оптимизировать бандл.
-
Не кладет лишнего в node_modules.
-
Дебажится и ремонтируется в домашних условиях.
В прошлой статье мы разобрались с самым холиварным вопросом — в 2025 году библиотеки уже можно публиковать в esm-only формате. Сегодня закончим тему сборки и выясним, нужны ли библиотекам:
-
Минификация
-
Транспиляция
-
Полифиллы
-
Сорсмапы
-
src прямо на npm
-
Бандлинг
Сегодня поговорим только про традиционное использование пакетов на node или через бандлер, сборку для CDN не трогаем. Поехали!
Минификация
Начнем с простого — стоит ли минифицировать код, который мы публикуем? Мы минифицируем код клиентской части приложений, чтобы грузить меньше данных по сети. Применимо ли это к библиотекам?
На размер итогового бандла приложения и скорость сборки такая пред-минификация не влияет. При сборке конечного проекта все библиотеки в любом случае пройдут минификацию. Сильно ускорить или улучшить этот процесс вы не сможете, потому что а) бандлер не знает, что эти файлы уже маленькие, и б) ваш код все равно надо дооптимизировать под конкретное использование (потришейкать / заинтайлить).
Есть ли еще аргументы? Да, с минифицированным кодом node_modules будут меньше, а установка быстрее. Но если у пользователя что-то идет не так, дебажить и чинить библиотеку гораздо сложнее (кто хоть раз ковырял обфусцированный код, тот знает).
Значит, минифицировать библиотеки не стоит. Зачем некоторые сборщики библиотек по умолчанию минифицируют код — для меня загадка.
Транспиляция
Транспиляция может значить несколько вещей: и компиляция compile-to-JS языков, и пересборка в более старую версию ES, например a?.b -> a == null ? undefined : a.b. Ответы для этих случаев будут немного отличаться.
Да, нам нужно сделать нормальный JS из нестандартного JS-синтаксиса — JSX, TS, Vue SFC, не дай бог ReScript или на чем там пишут детишки. Без этого наш код не заведется в node (хотя для TS это может скоро измениться), а бандлер придется специально настраивать, чтобы пересобрать библиотеку для браузера.
Более интересный вопрос — нужно ли транспилировать код в пониженную версии ES для более старых рантаймов. На сервере или локально (node / deno / bun) код с неподдерживаемым синтаксисом взорвется. Для браузера код пройдет через бандлер, но оттранспилируется ли он? В vite — да, весь код пройдет через esbuild. А для webpack базовый babel-loader явно рекомендуют для node_modules отключить. То есть возможность до-транспилировать код для браузера есть, но ее часто придется настраивать вручную.
Итого: библиотекам все таки нужно явно продумывать поддержку node и браузеров и транспилироваться под них. Но транспиляция — деструктивный процесс: пересобранная версия может раздувать бандл и хуже перформить в рантайме, а провернуть этот фарш назад уже не выйдет. Так что оптимально — выбрать достаточно современный таргет (node20 / chrome112), указать его в документации и в package.json поле engines. Если пользователи хотят поддерживать более старые браузеры — включат транспиляцию нашей библиотеки.
Полифиллы
Где транспиляция, там и полифиллы. Напомню, транспиляция — изменение синтаксиса, полифилл — добавление встроенных объектов или их методов. Например, в ES2025 есть Set.prototype.union, и наконец-то можно объединять множества.
Ситуация с полифиллами для библиотек печальная: они бы нам очень пригодились (методы в JS добавляют чаще, чем синтаксис), но использовать их мы не можем. Классический полифилл — это глобальный сайдэффект, который манкипатчит глобальные объекты (Set.prototype.union = Set.prototype.union || customUnion). Это создает проблемы:
-
Если наши пользователи используют другой полифилл, применится только тот, который подключили первым.
-
Добавить полифилл легко, а убрать ненужный — сложно.
-
Сайдэффекты мешают тришейкать код, потому что бандлер должен сохранить порядок всех модулей с сайдэффектами.
-
Это дополнительная зависимость библиотеки, которую надо иногда обновлять.
Отличных решений нет, предложу 2 варианта:
-
Руками использовать только объекты и методы, доступные в выбранных рантаймах. В этом помогут опция lib в tsconfig.json и юнит-тесты на целевых версиях рантаймов.
-
Явно задокументировать, какие не-общедоступные методы нужны библиотеке (но помнить, что документацию никто не читает).
Сорсмапы
Вспомним, что основная цель сорсмапов — читаемые стектрейсы в сильно преобразованном коде. Нужно ли это npm-пакетам?
Если мы минифицировали код и пытаетемся скомпенсировать это через сорсмапы — мы проиграли, потому что сорсмап содержит весь исходный код. Общий publish size вырастет и лишит нас единственного преимущества минификации.
Если код библиотеки попадает в браузер через бандлер, то наши сорсмапы не смержатся с сорсмапами приложения без приседаний вроде rollup-plugin-sourcemaps2.
Единственный кейс, в котором можно подумать про сорсмапы — библиотеки не на чистом JS / TS (например, JSX / Vue SFC). При этом помним, что из коробки эти сорсмапы применятся только в node.
Публиковать ли src?
Исходный код публиковать на npm не надо, если репозиторий доступен всем на GitHub (опенсорс) или на нашем рабочем гите, а эти пункты всегда должны выполняться.
Давайте пробежимся по возможным кейсам, где пользователям может понадобится src:
-
Посмотреть: если код не минифицирован и не сильно транспилирован, можно совершенно так же посмотреть на собранную версию. Если всё-таки сильно транспилирован, то на гитхабе.
-
Поредактировать, пересобрать и проверить на своем проекте: гораздо удобнее склонировать репу, поработать с ней и подключить к себе через
npm link. -
Подключить несобранную версию библиотеки и пересобрать самостоятельно как часть приложения. Это нездоровое желание, которое скорее всего говорит о том что официальная сборка сломана. Впрочем, если очень надо — клонируем репу в
src/npm-libи ставим черезnpm i src/npm-lib.
src — мусор в node_modules. Не публикуйте src.
Бандлинг
Самый холиварный вопрос — нужно ли бандлить библиотеки? Начнем с преимуществ, которые это может дать:
-
Ускорить сборку конечного приложения: бандлеру не нужно бандлить код библиотеки.
-
Ускорить dev-режим — на vite это не повлияет из-за dependency pre-bundling, для webpack может быть какой-то профит.
-
Ускорить импорт библиотеки в node — но он и так быстрый, например, 247 модулей из date-fns импортируются у меня за 0.6мс, трудно представить когда это будет критично.
-
Раньше бандлинг помогал инкапсулировать API библиотеки, потому что можно было напрямую импортировать что-нибудь приватное через
import { secretInternal } from 'lib/dist/secretInternal'. Сейчас проблема решается через exports в package.json
Теперь к минусам:
-
Для браузерных библиотек бандлинг может помешать разбить код библиотеки на несколько чанков (в общем случае деление одного js-модуля на несколько — небезопасная операция). Пример: в приложении есть
import { core } from 'lib'на всех страницах иimport { core, heavyExtra } from 'lib'в одном месте. ЕслиcoreиheavyExtraживут в одном js-файле, они скорее всего будут всегда грузиться вместе, а если в разных — есть хороший шанс грузитьheavyExtraтолько когда он правда нужен. -
Усложняет ремонт приложения, потому что редактировать один огромный файл сложнее, чем несколько маленьких.
-
Дополнительный шаг сборки библиотеки, но с этим можно смириться, если мы сэкономим время всем своим пользователям.
Скажу честно: ни плюсы, ни минусы бандлинга не кажутся мне очень убедительными. Единственное, в чем я уверен — ощутимая разница будет только а) для времени обработки бандлером, и б) только для очень больших библиотек. Отдельный вопрос — стоит ли бандлить .d.ts файлы. Вполне допускаю, что это полезно, но пока не готов глубоко рисерчить эту тему. В другой раз.
А, чуть не забыл: главное правило бандлинга библиотек — не вбандливайте зависимости. Представим: мы используем date-fns в библиотеке, наш пользователь использует date-fns в своем приложении. Если date-fns вбандлить в нашу библиотеку, к конечным пользователям поедет два date-fns. Нехорошо. Касается и обычных dependencies, и (особенно) peerDependencies.
Итого, как собирать библиотеки в 2025:
-
Не минифицируем: это усложняет дебаг библиотеки и не дает никакого профита.
-
Не публикуем сорсмапы: они не работают в бандлере и раздувают пакет.
-
Не используем полифиллы: они завязывают ваш код на глобальные сайд-эффекты и могут конфликтовать с полифиллами наших пользователей.
-
Не публикуем исходный код на npm: он и так доступен в нашем репозитории.
-
Транспилируем в чистый JS: не стоит заставлять пользователей страдать с настройкой бандлера.
-
Явно выбираем таргеты поддержки node и браузеров, транспилируем синтаксис для них (но не пытаемся поддержать самое старьё, это вредит большинству пользователей).
-
Не бандлим, пока это не тормозит сборку конечных приложений. Никогда не бандлим зависимости.
Уф, вроде разобрались. Остаюсь на связи, в следующий раз поговорим, какие инструменты стоит использовать для сборки библиотек.
ссылка на оригинал статьи https://habr.com/ru/articles/936010/
Добавить комментарий