Привет, Хабр! Я Валерий Маланин, фронтенд-разработчик в команде Modus BI. И по опыту знаю, что каждый разработчик хотя бы раз мечтал попасть на проект, где всё с нуля. Свежий стек, понятная архитектура, аккуратные модули, тесты, документация и никаких комментариев в духе «не трогать, иначе всё упадёт». В таком проекте легко писать новый код и приятно разбираться в старом.
Но в реальности всё обычно выглядит иначе. Команда приходит в продукт — а там React 16, Webpack 2, компонент на две тысячи строк, круговые зависимости и ни одного теста. И это не исключение, а обычная картина для живой системы, которая давно работает в проде.
Любой проект со временем накапливает легаси. Бизнес торопит и заставляет срезать углы. Команда меняется, и вместе с ней уходит контекст старых решений. Технологии устаревают, а код остаётся. В итоге систему становится страшно менять, потому что никто до конца не понимает, что сломается после очередной правки.
Легаси в этом смысле не аномалия и не признак плохой команды. Это естественное состояние продукта, который долго жил, рос и успел пережить не один этап развития. Вопрос не в том, как избежать легаси любой ценой, а в том, как взять его под контроль и начать улучшать систему без героизма, больших переписываний и лишних рисков.
Что такое легаси
Майкл Физерс в книге «Working Effectively with Legacy Code» дал простое определение: легаси — это код без тестов. Вне зависимости от возраста и технологий. Если код нельзя безопасно изменить — он уже легаси, даже если написан вчера.
Но есть и более широкий взгляд. Легаси-код — это унаследованный код, который продолжает работать в продакшене, но сложен в изменении: нет тестов, нет документации, технологии устарели или логика размазана без чёткой структуры. Это код, где решения, принятые три года назад из лучших побуждений, сегодня стали ежедневной проблемой.
И самое важное: легаси — это не аномалия и не чья-то личная ошибка. Это естественный результат живого продукта. Любая система, которая долго развивается, накапливает компромиссы. Бизнес требует выпускать фичи быстрее. Команда меняется. Контекст решений теряется. Поверх старой логики наслаивается новая. В какой-то момент кодовая база начинает хранить не только текущую реализацию, но и всю историю продукта вместе с её ограничениями.
Поэтому задача инженера — нем переписывать код с нуля, а признать, что легаси — это рабочая система, которая просто стала дорогой и рискованной в изменении. А значит, работать с ней системно, без паники и с пользой для продукта.
Изучение проекта и установка правил
Прежде чем что-то менять — нужно понять, что имеешь. Звучит очевидно, но на практике эту фазу часто пропускают. Разработчик приходит на проект, видит хаос и сразу хочет «навести порядок»: обновить зависимости, разрезать большой компонент, вынести бизнес-логику из UI, включить строгий линтер.. Результат предсказуем: что-то ломается, команда нервничает, а рефакторинг откатывается.
Шаг 1. Археология кода
Археология кода, или software archaeology, звучит громко, но по сути это обычная инженерная работа: система изучается слой за слоем, а команда пытается восстановить её реальную структуру. На этом этапе не нужно ничего улучшать — сначала составляется карта. Для этого следует:
Изучить структуру директорий и зависимости между модулями. Открыть package.json, посмотреть, какие библиотеки тянет проект, какие уже устарели, какие дублируют друг друга. Пройтись по папкам и понять, где живёт бизнес-логика, где утилиты, где UI, где слой работы с API. В легаси-проектах всё это часто перемешано: в components/ лежат и элементы интерфейса, и хелперы, и куски логики, которые вообще не должны были там оказаться.
Пример из практики
На одном из проектов Modus BI папка components/ содержала всё вперемешку — UI, бизнес-логику. Никаких подпапок, никакой группировки. Компонент кнопки лежал рядом с компонентами целых разделов. Прежде чем что-либо рефакторить, ушла неделя просто на то, чтобы составить карту: что от чего зависит, что используется, а что давно мёртвый код.
Найти точки входа и критические пути. Какие сценарии в приложении самые важные для пользователя? Какие модули участвуют в авторизации, оплате, сохранении данных, построении ключевых экранов? Это даст понимание, что трогать в последнюю очередь.
Определить «горячие» места. Самый простой способ — посмотреть git log и найти файлы, которые чаще всего менялись за последний год. Если файл трогали 200 раз — скорее всего, это и есть болевая точка проекта. Именно там скапливаются баги, именно там код становится наиболее хрупким.

-
Зафиксировать архитектуру «как есть». Даже если она далека от идеала, её нужно сделать видимой. На доске, в Miro, в диаграмме зависимостей, в обычном документе — неважно. Главное — вынести из голов в явный вид связи между слоями, сервисами, стором, API-клиентами и компонентами. Пока этой карты нет, команда работает вслепую и спорит не о системе, а о своих представлениях о ней.

На этом этапе важно смотреть не только на связи между модулями, но и на границы ответственности. Где заканчивается интерфейс и начинается бизнес-логика. Где данные преобразуются, а где только отображаются. Какие модули можно считать внутренними, а какие уже стали опорными точками для половины системы. Без этого любая попытка «почистить код» быстро превращается в случайный набор правок.
Шаг 2. Определение узких мест
Когда карта проекта готова, нужно понять, что сильнее всего тормозит разработку. Например, где команда теряет время, где чаще всего ошибается и какие участки кода делают любое изменение дорогим и рискованным. Вот типичные проблемы, с которыми сталкиваются на легаси-фронтенде:
Медленные сборки. Устаревшая билд-инфраструктура — один из самых болезненных тормозов. Каждый npm run dev или npm run build превращается в ожидание. Это не просто раздражает — это убивает рабочий ритм: пока разработчик ждёт сборку, он теряет контекст, отвлекается, переключается на другие задачи.
Пример из практики
На одном из проектов сборка дев-версии и прод-сборка занимали около 30 минут. Чтобы просто начать работу, нужно было полчаса ждать. После обновления сборщика с Webpack 2 до Webpack 5 время сборки сократилось примерно до минуты. Это не изменило бизнес-логику и не добавило ни одной новой фичи, но моментально изменило ритм работы всей команды.

Хрупкие места. Это участки, которые страшно трогать. Обычно там нет тестов, много неявных зависимостей и длинный хвост побочных эффектов. Любое изменение потенциально ломает что-то в другом конце приложения.
Модули без владельца. Код, за который никто не отвечает и который никто толком не понимает. Его писал человек, который давно ушёл, а документации нет. Почему здесь именно такой if на 471-й строке, никто и не помнит, но удалить не решаются.
Протечки между слоями. UI-компоненты знают о деталях API. Бизнес-логика размазана по компонентам вперемешку с разметкой. Стейт хранится где попало: часть в Redux, часть в локальном стейте, часть в URL-параметрах.
Если какой-то участок уже выглядит опасным, полезно посмотреть не только на код, но и на его поведение в проде. Какие ошибки там всплывают? Есть ли повторяющиеся инциденты? На что чаще всего жалуются пользователи? Иногда проблемное место видно не по размеру файла, а по тому, сколько нестабильности оно создаёт вокруг себя.
Задача этого этапа не в том, чтобы сразу всё исправить. Сначала нужно увидеть, где у проекта реальные точки напряжения.
Шаг 3. Установка правил для команды
Когда есть карта проекта и видно основные болевые точки — время установить правила. Не «навести порядок во всём проекте за спринт», а зафиксировать: как пишем новый код:
Описать архитектуру «как должно быть»
Даже если сейчас проект устроен иначе, у команды должен появиться ориентир. Нужно зафиксировать, где должна жить бизнес-логика, где проходит граница между UI и слоем работы с данными, какие зависимости между модулями допустимы, а какие нет. Например, компонент не должен напрямую знать о формате ответа API, а сервис — тянуть код представления.
Такие решения лучше фиксировать в ADR. Тогда они не остаются на уровне устных договорённостей и не зависят от того, помнит ли кто-то исходный замысел.
Ввести правила для нового кода
В этом случае базовый минимум:
ESLint и Prettier;
единые правила именования;
понятная структура компонентов и файлов;
общие договорённости по организации модулей.
Но есть нюанс. Эти правила не стоит сразу распространять на весь легаси-проект. Если прогнать строгий линтер по всей кодовой базе разом, команда получит огромный MR на сотни файлов, который никто не сможет нормально отревьюить. Формально код станет «чище», а по факту проект просто утонет в косметических правках.
Пример из практики
Мы один раз попробовали включить строгий ESLint-конфиг на весь проект сразу. Результат — около 4 200 ошибок и ни одного желающего это разгребать. Со второй попытки сделали иначе: новые правила применялись только к изменённым файлам, а старый код проверялся по минимальному конфигу. Через некоторое время значительная часть кодовой базы уже соответствовала новым стандартам — без единого героического MR.
Ограничить зависимости между слоями
Если не зафиксировать правила зависимостей, новый код очень быстро повторит старые ошибки. UI снова начнёт тянуть в себя бизнес-логику. Сервисы снова полезут в стор. Временные обходы снова превратятся в постоянную архитектуру.
Поэтому здесь важно заранее договориться:
какие слои могут зависеть друг от друга;
какие импорты считаются нормальными;
какие связи считаются нарушением архитектуры;
где проходят границы ответственности модулей.
Легаси редко возникает из-за одного плохого решения. Чаще оно накапливается через десятки мелких компромиссов, которые никто вовремя не остановил.

Подготовить онбординг-документацию
Новый разработчик должен быстро понять:
как проект устроен сейчас;
какие зоны в нём уже считаются проблемными;
по каким правилам пишется новый код;
куда лучше не заходить без дополнительного контекста.
Это экономит время на вхождение в проект и снижает риск случайных правок в самых хрупких местах.
На этом этапе задача не в том, чтобы срочно сделать проект аккуратным — нужно перестать плодить новый легаси поверх старого.
Модернизация
Легаси нельзя переписать за выходные. Но его можно постепенно улучшать. Ключевое слово — «постепенно».
Шаг 4. Выбрать стратегию модернизации
Команда приходит в старый проект, видит хаос, устаревший стек, странные зависимости, медленные сборки, модули без тестов — в итоге возникает соблазн всё выкинуть и написать с нуля. Но на практике это почти всегда дороже и рискованнее, чем кажется.
Старый код хранит не только неудачные решения, но и реальные знания о системе. В нём зашиты редкие бизнес-сценарии, старые ограничения, обходы под внешние интеграции, реакции на сбои, накопленные за годы работы в проде. Часто это выглядит как странный if, лишняя проверка или неочевидная ветка в обработке данных. Но за каждой такой конструкцией обычно стоит не чья-то прихоть, а опыт прошлых инцидентов.
При переписывании с нуля команда почти неизбежно теряет часть этого контекста. Новая система выглядит чище, но в ней ещё нет того запаса «боевой» устойчивости, который старая кодовая база получила за годы эксплуатации.
Вторая проблема — big rewrite редко происходит в вакууме. Пока одна часть команды строит новую систему, старая продолжает жить в проде. Её нужно поддерживать, чинить, синхронизировать по логике и данным. В какой-то момент появляются две системы сразу: одна уже сложная, но рабочая, и вторая — ещё не зрелая, но потребляющая ресурсы. Это резко повышает стоимость изменений и почти всегда растягивает миграцию дольше, чем планировалось.
С инфраструктурной точки зрения такой подход тоже опасен. Новая система требует заново выстроить сборку, деплой, конфигурацию окружений, логирование, мониторинг, алерты, rollback-сценарии, доступы и эксплуатационные процедуры. То есть команда переписывает не только код, но и весь операционный контур вокруг него. Если сделать это одним рывком, риск регрессий, сбоев на выкладке и расхождений в поведении становится слишком высоким.
Поэтому в легаси почти всегда выигрывает постепенная замена по частям. Нужно выбрать участок, зафиксировать его поведение, обложить проверками, вынести рядом новую реализацию и безопасно переключить трафик, когда она будет готова.
Шаг 5. Паттерн Strangler Fig
Название этого паттерна пришло из ботаники. Фиговое дерево-душитель растёт вокруг другого дерева, постепенно его обвивает и со временем занимает его место. Ровно так же работает этот паттерн с кодом — не вырывает старую систему одним движением, а заменяет её по частям, пока новая логика не возьмёт на себя весь нужный функционал.
Обычно этот процесс выглядит так:
Заморозить старый участок. Команда договаривается, что в старый модуль больше не добавляется новая функциональность. Его не развивают, а только поддерживают: исправляют баги, закрывают критические инциденты и следят, чтобы он не ломал остальную систему.
Поднять новую реализацию рядом. Новый модуль живёт в соседней директории — со своей структурой, правилами и нормальными границами ответственности. Он может читать данные из старого модуля или использовать существующие контракты, но не наоборот.
Переключать нагрузку постепенно. Это можно делать через фича-флаги, маршрутизацию, конфигурацию окружения или частичное включение для части пользователей. Главное — не переводить все сразу, пока система не стабилизировалась.
-
Смотреть на поведение в проде. После переключения важно проанализировать:
выросло ли число ошибок;
не просело ли время ответа;
нет ли деградации по ключевым сценариям;
как ведут себя логи, метрики и алерты;
не появились ли различия между старой и новой реализацией.
Удалить старый код. Когда новый модуль заработал, старый можно убирать: удалять код, вычищать зависимости, убирать старые конфиги, маршруты и временные прослойки. До этого момента миграция не считается завершённой.
Этот подход отлично работает на фронтенде. Его применяли при переходе от монолита к микрофронтендам, при обновлении React-приложений по частям.
Пример из практики
Мы в Modus применяли этот подход при миграции визуализации с движка PlotlyJS на AMCharts. Сначала написали новый модуль рядом со старым. Некоторое время они жили вместе на проде, но новой версией пользовалось только ограниченное количество людей — остальные продолжали работать на старой. Когда все баги были исправлены и метрики показали стабильность, старый модуль удалили одним MR. Больше 3 000 строк — в корзину.

Шаг 6. Тесты как основа безопасного рефакторинга
Перед тем как менять легаси-код, нужно зафиксировать его текущее поведение. Не после рефакторинга, не параллельно с ним, а до любых заметных изменений. Это принципиальный момент — пока у команды нет страховки, любое улучшение остаётся догадкой.
Здесь хорошо работают characterization tests — тесты, которые фиксируют фактическое поведение системы. Разработчик берёт существующий модуль, прогоняет через него реальные входные данные, смотрит, что происходит на выходе, и закрепляет этот результат тестами. Такой подход особенно полезен там, где логика давно обросла исключениями, побочными эффектами и неочевидными ветками, а документации либо нет, либо она давно не совпадает с кодом.
В легаси это важно по двум причинам:
Тесты помогают понять, что именно делает код. Пока логика спрятана в большом модуле без проверок, команда видит только структуру файла. Как только появляются тесты на реальные сценарии, система становится понятнее.
Тесты дают минимальную страховку перед изменениями. Если после рефакторинга поведение изменилось, команда увидит это сразу, а не через баг-репорт из продакшена в конце недели.
Но здесь важно не впасть в другую крайность и не пытаться сначала построить идеальную тестовую пирамиду на весь проект. В легаси это почти всегда не работает. На старте задача проще:
зафиксировать текущее поведение в самых рискованных местах;
покрыть основные пользовательские и системные сценарии;
проверить участки, где уже были инциденты или частые регрессии;
дать команде возможность менять код без слепого страха.
Если система особенно чувствительная, одних тестов на уровне кода может быть мало. Тогда до рефакторинга полезно дополнительно проверить, как модуль ведёт себя в окружении: какие логи он пишет, какие ошибки уже всплывают, какие метрики могут показать деградацию после изменений.
Пример из практики
В одном из старых модулей часть логики выглядела как набор случайных условий, которые за годы никто не решался трогать. Перед изменениями сначала зафиксировали текущее поведение тестами на реальных входах, а уже потом начали разбирать код на части. В процессе выяснилось, что несколько странных веток отвечали за редкие, но реальные сценарии, которые без тестов было бы очень легко потерять.
Без тестов рефакторинг легаси быстро превращается в русскую рулетку. Можно угадать и ничего не сломать. А можно убрать «лишнюю» проверку, которая годами защищала систему от редкого, но болезненного сбоя. Поэтому в старом проекте тесты — это минимальное условие для безопасных изменений.
Шаг 7. Инкрементальный рефакторинг
После фиксации текущего поведения модель можно менять. Но здесь важно не сорваться в большой рефакторинг сразу. В легаси почти всегда выигрывают маленькие итерации: один модуль, один участок логики, один понятный MR. Чем меньше объём изменений, тем проще их проверить, отревьюить, откатить и связать с причиной, если после выкладки что-то пошло не так.
Главное правило — не смешивать рефакторинг и новую функциональность в одном MR. Если в одном изменении команда одновременно переписала старый модуль, обновила зависимости и добавила новую фичу, потом почти невозможно быстро понять, что именно стало источником проблемы. В проде такие истории заканчиваются одинаково: система ведёт себя нестабильно, команда тратит время не на исправление, а на поиск виновника.
Поэтому инкрементальный рефакторинг обычно строится так:
Выбрать один проблемный участок. Не весь проект или раздел, а конкретный модуль, компонент или кусок логики, который действительно мешает двигаться дальше.
Зафиксировать его текущее поведение. Через тесты, логи, метрики, иногда через ручную проверку критических сценариев, если участок особенно чувствительный.
Внести одно понятное изменение. Например, вынести бизнес-логику из UI-компонента, разорвать неудачную зависимость, раздробить один монолитный модуль на несколько частей или убрать дублирование.
Проверить и выкатить отдельно. Не прятать такую правку внутри большого релиза, а дать ей пройти понятный цикл проверки и выкладки.
Только потом идти к следующему шагу. Не пытаться «добить всё до конца», пока не стало ясно, что предыдущий кусок пережил изменение нормально.
Приоритизация здесь тоже важна. Начинать лучше с того, что сильнее всего мешает команде. Если медленная сборка убивает продуктивность — обновите билд-инфраструктуру первой. Если один компонент-монстр на 2000 строк генерирует 80% багов — начните разбивать его.
С инженерной точки зрения у маленьких изменений есть ещё один плюс: они лучше живут в поставке. Небольшой MR проще ревьюить, его проще прогнать через CI или откатить, если после релиза всплывёт регрессия.
Пример из практики
На одном проекте команда долго откладывала разбор крупного компонента, потому что он казался слишком сложным для нормального рефакторинга. В итоге вместо одной большой задачи работу разбили на несколько коротких шагов: сначала вынесли один кусок логики, потом разорвали лишнюю зависимость, затем покрыли отдельную часть тестами и только после этого начали дробить сам компонент. Такой подход занял больше календарного времени, но не сломал прод и не парализовал команду.
Типичные проблемы и что с ними делать
Вот конкретные ситуации, с которыми вы скорее всего столкнётесь на легаси-фронтенде:
Протечки между слоями. UI-компоненты напрямую ходят в API, знают формат ответов и сами преобразуют данные перед рендером. Бизнес-логика живёт вперемешку с разметкой, а слой данных зависит от представления. В такой системе сложно менять что-то изолированно: правка в одном месте быстро расползается по соседним слоям.
Что с этим делать:
зафиксировать границы между слоями;
вынести работу с API и преобразование данных в отдельные сервисы или адаптеры;
оставить компонентам только то, что относится к отображению и локальному поведению интерфейса.
Нет внятного управления состоянием. Часть данных живёт в локальном состоянии, часть — в сторе, часть — в URL, а часть вообще передаётся через props на несколько уровней вниз. Пока приложение маленькое, это ещё терпимо. Но со временем такая схема начинает ломаться: данные дублируются, зависимости становятся неочевидными, а любое изменение в потоке состояния даёт побочные эффекты.
Что с этим делать:
определить, где должен жить каждый тип состояния;
не пытаться мигрировать всё сразу;
сначала наводить порядок в новом коде и только потом постепенно разбирать старые участки.
Устаревший стек и старая инфраструктура сборки. Старые версии сборщика, старые версии фреймворка, залежавшиеся полифиллы, нестабильный dev-сервер, долгая сборка, сложная конфигурация окружений. Формально система работает, но сама среда разработки уже замедляет команду и повышает риск ошибок на поставке.
Что с этим делать:
начинать не с полной замены всего стека, а с самых дорогих точек;
сначала упростить и ускорить билд-инфраструктуру;
потом двигаться к обновлению зависимостей и фреймворка;
после каждого шага проверять совместимость и поведение системы в реальных сценариях.
Нет единых правил code style. В одном файле табы, в другом — пробелы. Где-то var, где-то const. Компоненты и модули организованы по разным принципам, а названия функций зависят только от привычек автора. Это не главная проблема проекта, но такой фон сильно усложняет чтение и ревью.
Что с этим делать:
вводить ESLint и Prettier;
применять правила сначала только к новому и изменённому коду;
не превращать наведение порядка в гигантский косметический MR.
Монолитные компоненты и модули. Один компонент рендерит интерфейс, хранит состояние, валидирует форму, ходит в API и заодно отправляет аналитику. Один модуль знает слишком много о соседних частях системы и давно перестал быть чем-то одним. Такой код трудно тестировать, переиспользовать и почти невозможно безопасно менять.
Что с этим делать:
сначала зафиксировать текущее поведение;
потом выносить по одной логической части за итерацию;
после каждого шага проверять, что система ведёт себя так же, как раньше;
не пытаться разрезать монолит одним большим MR.
Модули без владельца. Никто не знает, зачем нужен этот кусок кода, но все боятся его трогать. Обычно такие места появляются там, где несколько раз менялась команда, а знание о системе так и не было вынесено из голов в документацию, тесты или понятные архитектурные решения.
Что с этим делать:
начинать не с переписывания, а с восстановления контекста;
смотреть историю изменений, старые MR, инциденты и связанные задачи;
фиксировать найденное в документации и тестах, чтобы модуль перестал быть «чёрным ящиком».
Важный момент — не нужно решать все эти проблемы одновременно. Легаси почти никогда не чинится одним архитектурным усилием. Нужно выбрать самый дорогой источник боли, зафиксировать поведение, аккуратно изменить, проверить результат и только потом переходить к следующему участку.
Заключение
Легаси — это не приговор и не стыд. Это результат живого продукта. Если ваш код стал легаси — значит, продукт достаточно долго жил, чтобы приносить пользу. Это хороший знак.
Задача инженера — не страдать и не мечтать о переписывании с нуля. А системно улучшать: понять что имеешь, зафиксировать правила, покрыть тестами, менять по частям.
Работа с легаси учит вещам, которые не получишь на проекте с чистого листа: понимать чужие решения, думать о тех, кто придёт после тебя, работать с ограничениями и находить элегантные решения в неидеальных условиях.
Хороший TechLead не избегает легаси — он делает так, чтобы текущий код не стал легаси завтра. А если уже стал — знает, как с этим работать.
P.S. Присоединяйтесь к нашему BI-сообществу в Telegram и будьте в курсе последних новостей Modus!