Привет, Хабр! Меня зовут Василий Беляев. Я руководитель группы разработки по направлению фронтенда в ИТ-компании «Криптонит». В этой статье я расскажу про организацию работы с версиями и компонентами, оптимизацию рабочего процесса внутри команды, а также опишу несколько лайфхаков, которые мы применили.
В прошлый раз мы остановились на том, что оптимизировали package.json и избавились от прямых импортов. Это было необходимое, но недостаточное условие. Прежде, чем рассказывать о следующих шагах, я отвечу на те вопросы, которые подвисли с прошлой статьи.
Почему мы используем vite?
На самом деле всё просто. При инициализации проекта через create vue@latest по умолчанию ставится сборщик vite. На мой взгляд, у него есть ряд плюсов перед webpack. Главный из них заключается в том, что, с точки зрения Developer Experience, значительно быстрее работает hot reload, особенно для объёмных проектов.
Почему мы выбрали Yarn?
Дополнительно к Node.js ставится пакетный менеджер. Это инструмент, который автоматизирует установку, обновление и удаление сторонних библиотек (пакетов), а также управляет их версиями и зависимостями. Без него разработчику пришлось бы вручную скачивать все необходимые файлы, отслеживать их совместимость и переносить изменения при обновлениях. Он также позволяет запускать вспомогательные скрипты (сборка, тестирование), указанные в поле scripts файла package.json.
По умолчанию в качестве пакетного менеджера ставится npm, также существуют Yarn Classic (1.x), pnpm, Bun, Yarn Berry (2.x и выше).
Мы раньше использовали npm, но потом перешли на Yarn по разным причинам. Для меня появился очень важный момент, связанный с подходами при работе с peerDependencies при установке зависимостей.
При работе с npm устанавливаются все зависимости, указанные в peerDependencies. В некоторых случаях это может вызвать конфликты версий при установке пакетов и привести к неожиданным результатам. Также это может повлиять на скорость развёртывания на prod стенде, так как с дополнительным флагом (--prod/--production) они тоже устанавливаются.
Мы провели эксперимент, выполнив следующие действия и зафиксировав количество установленных зависимостей:
1. Инициировали проект (для этого требуется node, npm версии 7.x и выше, Yarn classic) c помощью команды npm create vue@latest (из официальной документации vuejs);
2. Выбрали TypeScript, Vue Router, Pinia, Vitest, End to End testing, ESLint.
3. Перешли в папку с проектом.
4. Установили зависимости c помощью команды npm i и посчитали их.
5. Удалили package-lock.json и папку node_modules, выполнили команду npm i --production.
6. Удалили package-lock.json и папку node_modules, отправили команду yarn.
7. Удалили yarn.lock и папку node_modules, выполнили команду yarn --prod.

В качестве дополнительного подтверждения откроем документацию npm и найдём цитату: «As of npm v7, peerDependencies are installed by default». То есть, все зависимости, указанные в peerDependencies, устанавливаются npm по умолчанию. В Yarn такого не происходит.
Данный момент можно обойти с помощью блока peerDependenciesMeta, указав, что зависимость НЕ является обязательной. Из минусов такого подхода — нет информации про уведомления. При использовании Yarn peer-зависимости не устанавливаются автоматически, но выдают в консоли предупреждения.
Думаю, теперь вы представляете наш стек и понимаете, почему он такой. Перейдём к разбору дальнейших шагов проделанной оптимизации.
Убираем проблемы с совместимостью
После того как мы разобрались со скоростью сборки и размерами бандлов, у нас остались ещё несколько проблем:
«зоопарк» версий по core-зависимостям в библиотеках и проектах;
периодическая потеря обратной совместимости библиотек между собой.
Для решения этих проблем мы создали две библиотеки: dev-config и dev-core.
Убираем зоопарк версий core-зависимостей
Для того, чтобы решить проблему с зоопарком не только в библиотеках, но и в конфигах и синхронизации версии библиотек, мы создали библиотеку dev-config.
Раньше было довольно сложно между проектами и библиотеками переносить не только конфигурационные файлы, но иногда и заимствовать решения, так как была примерно следующая ситуация по проектам:

То есть в разных проектах использовались разные версии core-библиотек. Например, довольно большая разница между ESLint 8 и ESLint 9 в конфиг-файлах, а во Vue 3.3 появились новые фичи, которых ещё нет во Vue 3.0.
Что мы поместили в dev-config?
Все библиотеки из проектов и библиотек, которые встречаются более одного раза. Тем самым мы синхронизировали везде версии библиотек. То есть если везде установлена единая версия dev-config, то мы знаем, что версии одинаковые.
Единые конфигурационные файлы для вспомогательных инструментов — Vite, ESLint, TypeScript и т.д. Одно из самых интересных решений: мы теперь знаем, что вспомогательные инструменты у нас единой версии, и создали единые конфиги для всего! Как это работает? При создании проекта мы по умолчанию подключаем конфиги из dev-config и в случае необходимости перезаписываем определённые правила, например, для ESLint. Дополнительно с помощью ESLint мы покрыли огромное количество договоренностей по code-style, что позволяет отслеживать их на этапе проверки в CI/CD и в полуавтоматическом режиме править код в соответствии с нашими правилами, принятыми внутри команды для общего удобства.
Вынесли туда CI/CD для деплоя проектов и библиотек. Ещё один интересный момент. Мы сделали две конфигурации CI/CD: одна для библиотек, вторая для проектов. При этом мы вынесли большое количество опций в переменные, с помощью которых можно отключить часть линтеров по необходимости, а также определять дополнительные параметры, например путь в Nexus, куда нужно положить библиотеку или проект.
Создали шаблоны для проектов и библиотек. Теперь, когда мы создаём новую библиотеку или проект, мы создаём новый репозиторий из одного из этих шаблонов. Это довольно удобно, так как не надо тратить время на первоначальную настройку.
Что в итоге получилось с точки зрения зависимостей? В библиотеки мы стали тянуть dev-config как devDependencies, а в проекты — уже как основную зависимость.

При этом у нас в библиотеках и проектах сократился package.json, упростилась работа по отслеживанию зависимостей – большая часть пакетов ушла в dev-config. Если раньше в проекте в package.json тянулось около 40 – 60 зависимостей, то сейчас стало значительно меньше. Пример на библиотеке:

Дополнительно мы подключили кэширование результатов выполнения части работы в рамках CI/CD и ускорили процессы.
Реализуем поддержку обратной совместимости наших библиотек
При активном развитии и рефакторинге библиотек стали возникать вопросы о совместимости библиотек между собой. То есть условный Datepicker мог не поддерживать изменения из других библиотек. Регулярно были вопросы на тему совместимости:

В dev-core мы поместили совместимые версии библиотек и перенесли управление версиями библиотек с проектов на dev-core. Добавили сборку композитного Storybook, чтобы дизайнеры, мы и аналитики могли смотреть на актуальные компоненты.
Как теперь выглядит схема подключения в проекты:

После реализации dev-core дополнительно изменили package.json

Так выглядит общий результат, который упростил жизнь разработчикам:

Дополнительно фиксируем процессы!
Проблема с рабочим процессом (workflow) была связана с тем, что все хотели изменений для своих проектов «здесь и сейчас», и иногда они противоречили друг другу. Спустя несколько итераций мы пришли к такому алгоритму:
0. Требуется доработки? Начинаем смотреть: соответствует UI-KIT, или нет?
1. Если нет, создаём запрос к дизайнерам.
2. Заводим задачу на реализацию (или баг, если есть ошибка).
3. Делаем задачу и создаём merge request.
4. Проводим кросс-командное ревью.
5. Выпускаем новые версии библиотек.
6. Создаём MR в dev-core.
7. Проверяем, все ли работает и совместимо.
8. Если нет, возвращаемся к пункту 2.
9. Если все работает, выпускаем dev-core.
10. Используем dev-core на проектах.
Что мы ещё добавили для оптимизации (бонусные лайфхаки)
Добавили использование кэша GitLab Runners — он позволяет сократить время на переустановку зависимостей в CI. Зависимости переустанавливаются только в том случае, если были изменения в package.json (добавили, убрали зависимость или изменили версию) — в случае, если мы исправили опечатку или сделали hot-fix — этот шаг пролетает буквально за несколько секунд – инициализация джобы, проверка зависимостей (из кэша) – все OK, обновлять не надо — завершаем.
Добавили проверку на соответствие peerDependencies в CI. Если у нас есть несоответствия в зависимостях проекта несоответствие peerDependencies из dev-core, то CI падает и выдаёт список невалидных версий библиотек. Но… мы оставили возможность перезаписывать версии библиотек на усмотрения проекта через overrides и resolutions.
Для того, чтобы не плодить instance’ы одной и той же библиотеки (что иногда может вызвать проблемы, например — AGGrid) мы добавили в процесс сборки наших библиотек вырезание бандлов cпомощью core-зависимостей. Теперь при установке библиотеки у нас получается, что устанавливается OpenLayers, но он не входит в бандл карт, а ссылается на зависимость OpenLayersиз node_modules.
Общие итоги
Мы сократили время сборки всех библиотек на 70.1% — с 45 минут до 13 (без использования кэша). С кэшем — ещё быстрее.

Мы сократили общий вес бандлов на 95,8% — с 68,3 Мб до 2,9 Мб, что повлияло и на время сборки библиотек.

Мы стабилизировали процессы и взаимодействие между разработчиками и дизайнерами, проводя все изменения через UI-KIT. И это все при активном развитии библиотек.
Выводы
Проделанная работа вышла за рамки простого «наведения порядка». Мы не просто ускорили сборку и уменьшили бандлы (хотя это само по себе отличный результат), но и фундаментально изменили подход к управлению зависимостями и версиями.
Создание библиотек dev-config и dev-core позволило нам перейти от разрозненных версий инструментов и конфликтующих компонентов к единой и предсказуемой системе. Теперь мы можем быть уверены, что, если мы используем dev-core на одном продукте, то он будет так же работать и во всех остальных, а настройка нового проекта или библиотеки занимает минуты, а не часы.
Ещё одно важное, на мой взгляд, достижение — это изменение подхода к работе над общими библиотеками для наших продуктов. Фиксация workflow и жёсткая привязка к UI-KIT сделали процесс разработки прозрачным. Они на 99% устранили ситуации, когда «у меня всё работало, а после твоего обновления сломалось».
Работа над оптимизацией и организацией библиотек показала: к ним стоит относиться как к внутренним продуктам. Ведь конечные пользователи здесь — мы сами, фронтенд-разработчики.
