Ты приходишь в большую компанию, тебе дают доступ к корпоративному монорепо, ты пишешь git clone и идёшь за кофе. Возвращаешься - всё ещё качается. Восемь гигабайт, два миллиона файлов, пятнадцать лет истории. Клон занял двадцать минут, а потом начинается настоящая боль: git status думает по три секунды на каждый вызов, git checkout другой ветки - десять секунд, IDE на каждое сохранение файла подвешивает редактор. Это не сломанный Git и не кривые руки. Это физика: операции, которые на репозитории из тысячи файлов незаметны, на миллионе файлов превращаются в ощутимую задержку.
Большой репозиторий git - это не обязательно плохая архитектура. Linux kernel, Chromium, Windows, любой крупный монорепозиторий - там по определению гигабайты и миллионы объектов. Вопрос не в том, как сделать репо маленьким (часто это невозможно или нежелательно), а в том, как git ускорить на таком масштабе, не теряя удобство. К 2026 году в самом git собран полный набор инструментов ровно под эту задачу, и их даже не надо включать по одному - есть команда, которая включает всё разом. Разберём механику каждого слоя, потому что без понимания механики ты не поймёшь, какой инструмент решает какую конкретную боль.

Откуда берётся тормоз: три источника медленности
Чтобы лечить, надо понять диагноз. Медленность большого репо складывается из трёх независимых источников, и лечатся они тремя разными инструментами.
Источник 1 - объём данных при клоне и fetch. Полный клон тащит ВСЮ историю всех файлов: каждую версию каждого blob за все годы. Если файл переписывали тысячу раз, ты качаешь тысячу его состояний, хотя тебе нужно одно - текущее. Это лечит partial clone: качаем дерево коммитов и каталогов, а содержимое файлов (blob-объекты) подтягиваем по требованию.
Источник 2 - размер рабочего каталога. Даже если данные скачаны, два миллиона файлов на диске - это два миллиона записей, которые git checkout должен материализовать, а git status - обойти. Лечит sparse-checkout плюс sparse-index: на диск кладём только нужные тебе подкаталоги, а индекс не раздуваем до полного дерева.
Источник 3 - сканирование файловой системы. git status на каждый вызов обходит рабочее дерево и спрашивает у ОС mtime каждого файла, чтобы понять, что изменилось. Миллион stat-вызовов - вот твои три секунды. Лечит FSMonitor: фоновый демон слушает события файловой системы и говорит git, какие файлы вообще трогали с прошлого раза.
Поверх этих трёх - ускорители чтения метаданных: commit-graph (быстрый обход истории) и multi-pack-index (быстрый поиск объекта среди многих пакфайлов). Их обслуживает фоновое git maintenance. Теперь по каждому слою вглубь.
Partial clone: blob по требованию и git backfill
Partial clone стабилен с git 2.27, это давно не экспериментальная новинка. Идея: при клоне применяем фильтр, который говорит серверу не присылать определённый класс объектов. Самый частый фильтр - blobless:
Код: Выделить всё
git clone --filter=blob:none https://git.example.com/monorepo.git
Посмотрим, что записалось в конфиг после такого клона:
Код: Выделить всё
$ git config --get-regexp 'remote|partial'
remote.origin.url https://git.example.com/monorepo.git
remote.origin.fetch +refs/heads/*:refs/remotes/origin/*
remote.origin.partialclonefilter blob:none
remote.origin.promisor true
Есть тонкость, на которой спотыкаются. Команды, которые трогают много старых версий файлов, спровоцируют шквал мелких lazy fetch и будут тормозить хуже, чем на полном клоне. Классика - git log -p по всей истории файла или git blame на древнем файле: git начнёт по одному докачивать сотни исторических blob. Поэтому если тебе предстоит тяжёлый офлайн-анализ истории, blobless-клон не лучший выбор.
Чтобы не страдать от россыпи мелких докачек, с git 2.49/2.50 есть git backfill - управляемая массовая дозагрузка недостающих blob:
Код: Выделить всё
$ git backfill
Backfilling blobs: 100% (48213/48213), done.
$ git backfill --sparse
Sparse-checkout и sparse-index: только нужные пути
Допустим, монорепо содержит сто сервисов в каталогах services/, а ты работаешь над тремя. Зачем тебе на диске остальные девяносто семь? Sparse-checkout говорит git материализовать на диск только заданные пути:
Код: Выделить всё
$ git sparse-checkout init --cone
$ git sparse-checkout set services/auth services/billing libs/common
$ git sparse-checkout list
libs/common
services/auth
services/billing
После set на диске останутся только три указанных подкаталога плюс файлы в корне. Остальные исчезнут из рабочего дерева - но НЕ из истории, они всё так же в репозитории. Вернуть можно в любой момент новым set.
Но тут была вторая половина проблемы. Даже когда на диске три каталога, индекс (.git/index) по умолчанию всё равно содержит записи на ВСЕ два миллиона файлов - просто помеченные как "не в рабочем дереве". И git status снова обходит этот раздутый индекс. Решение - sparse-index:
Код: Выделить всё
$ git sparse-checkout init --cone --sparse-index
Проверить, что индекс действительно разреженный, можно так:
Код: Выделить всё
$ git ls-files --sparse | grep '/$' | head -3
services/api/
services/cache/
services/cdn/
FSMonitor: мгновенный git status
Третий источник тормозов - сканирование диска. Даже с sparse-checkout, если в твоём конусе пятьдесят тысяч файлов, git status честно опросит ОС про каждый. FSMonitor (file system monitor) переворачивает логику: вместо "опросить всё" - "узнать у демона, что менялось".
Встроенный fsmonitor стабилен и больше не экспериментальный, поднимается одной настройкой:
Код: Выделить всё
$ git config core.fsmonitor true
$ git config core.untrackedCache true
Разница на большом дереве драматическая. Реальный замер на монорепо с парой миллионов файлов:
Код: Выделить всё
# без fsmonitor
$ time git status
real 0m4.812s
# с включённым fsmonitor (демон прогрет)
$ time git status
real 0m0.231s
Код: Выделить всё
$ git fsmonitor--daemon status
fsmonitor-daemon is watching '/Users/you/monorepo'
Scalar: всё это одной командой (и это часть самого git)
Главный миф, который надо убить сразу: scalar - это НЕ внешняя обёртка от Microsoft, которую надо где-то скачивать. Scalar встроен в сам git с версии 2.38. Ты уже его установил вместе с git. Проверь:
Код: Выделить всё
$ git scalar version
$ which scalar
/usr/local/bin/scalar
Код: Выделить всё
$ git scalar clone https://git.example.com/monorepo.git
- делает partial clone с --filter=blob:none - быстрый старт без скачивания всех blob;
- включает sparse-checkout в cone-режиме и sparse-index - на диск только корень, дальше добавляешь пути сам;
- включает core.fsmonitor - мгновенный status с первого дня;
- настраивает commit-graph и multi-pack-index для быстрого чтения метаданных;
- регистрирует репо в фоновом git maintenance, чтобы обслуживание шло само.
Код: Выделить всё
$ cd existing-monorepo
$ git scalar register
$ git scalar list
/Users/you/existing-monorepo/src
Maintenance: фоновое обслуживание вместо ручного gc
Последний кусок мозаики - чтобы быстрые чтения оставались быстрыми, метаданные надо периодически переупаковывать. Раньше это был ручной git gc. В 2026 правильный путь - git maintenance, фоновое инкрементальное обслуживание:
Код: Выделить всё
$ git maintenance start
Зачем это в контексте скорости: commit-graph превращает обход истории (git log, git merge-base, проверки достижимости) из чтения тысяч коммит-объектов в чтение одного компактного файла с предпосчитанными generation numbers. Multi-pack-index даёт единый индекс поверх всех пакфайлов, чтобы поиск объекта не перебирал десятки .idx-файлов. Без фонового обслуживания эти структуры устаревают, и скорость деградирует.
Когда это нужно, а когда из пушки по воробьям
Честный разбор - потому что включать sparse-index на репо из пятисот файлов бессмысленно и только добавит сложности.
- Репо меньше пары сотен мегабайт и десятков тысяч файлов - не трогай ничего, git и так быстрый. FSMonitor и sparse-index тут дадут ноль выигрыша, а граблей добавят.
- git status стал заметно медленным (секунда и больше), но репо не гигантское - включи только core.fsmonitor. Это самая дешёвая и безопасная оптимизация, эффект мгновенный.
- Долгий клон и не нужна вся история файлов - --filter=blob:none, при необходимости git backfill для нужных путей.
- Большой монорепо, работаешь над частью - полный набор через git scalar clone. Не собирай его руками по кусочкам, scalar сделает это правильнее.
- CI-раннер, которому нужен только текущий снимок - сюда же blobless или treeless partial clone плюс sparse-checkout на нужный сервис; история ему не нужна вообще.
Мини-лаба: пощупать каждый слой руками
Возьми любой публичный крупный репозиторий (подойдёт зеркало git, llvm, любой ваш рабочий монорепо).
- Шаг 1. Замерь полный клон по времени и размеру: git clone URL full && du -sh full/.git.
- Шаг 2. Сделай blobless-клон: git clone --filter=blob:none URL lazy. Сравни время и du -sh lazy/.git. Загляни в git config remote.origin.partialclonefilter - убедись, что там blob:none.
- Шаг 3. В каталоге lazy включи sparse-index и оставь один подкаталог: git sparse-checkout init --cone --sparse-index, потом git sparse-checkout set ПУТЬ. Проверь git ls-files --sparse | grep '/$' - увидишь sparse-directory entries.
- Шаг 4. Включи fsmonitor: git config core.fsmonitor true. Сделай git status (поднимет демон), затем time git status дважды и сравни со временем без демона.
- Шаг 5. Дотяни blob для конуса: git backfill --sparse. Убедись, что после этого checkout по конусу не делает мелких докачек.
- Шаг 6. Сравни всё это с git scalar clone URL scalar-clone - и посмотри git config -l в склонированном репо: найди partialclonefilter, core.fsmonitor, sparse-checkout, maintenance. Убедись, что scalar включил то же, что ты собирал руками.
- 1. Чем blobless partial clone (--filter=blob:none) отличается от sparse-checkout по тому, ЧТО именно экономится - данные на скачивании или файлы на диске? Могут ли они работать вместе?
- 2. Почему sparse-checkout без sparse-index не ускоряет git status на монорепо, а с sparse-index ускоряет? Что меняется в .git/index?
- 3. За счёт чего FSMonitor превращает git status из секунд в доли секунды - что именно перестаёт делать git?
- 4. Scalar - это внешний инструмент Microsoft, который надо отдельно ставить, или часть git? С какой версии и что конкретно включает scalar clone?
Боль большого репо складывается из трёх независимых источников: объём при клоне, размер рабочего дерева, сканирование ФС. Им соответствуют partial clone с git backfill, sparse-checkout с sparse-index и FSMonitor; сверху - commit-graph, multi-pack-index и фоновое git maintenance. Каждый слой можно включить отдельно по диагнозу, а можно одной командой git scalar clone или git scalar register - и помни: scalar встроен в git с 2.38, это не внешняя обёртка. Не оптимизируй вслепую: замерь, найди источник, включи нужный слой. На настоящем монстре эффект - с минут до секунд и с секунд до миллисекунд.