Миграция JS-проекта на TypeScript: цифры до и после
Миграция ощущается как замедление: билд дольше, разработчики жалуются на ошибки типов. Разбираем, что реально измерить до и после перехода и как отделить пользу от накладных расходов.
Перевод проекта на TypeScript почти всегда обсуждают в терминах «стало надёжнее», «код читается лучше». Это не измеряется и не убеждает того, кто держит сроки релизов. Измеряется другое: сколько секунд теперь идёт билд, на сколько вырос холодный старт CI, сколько времени редактор думает над автодополнением в большом файле и сколько багов типов ловится до продакшена. Разберём, что снимать до старта, что после, и где TypeScript честно ускоряет, а где добавляет минуты, которые надо чем-то окупить.
Речь о TypeScript 5.x и типичном фронтенд- или Node-проекте на 100–500 тысяч строк. Замеры зависят от железа и структуры проекта, поэтому дальше — не универсальные числа, а методика: что мерить, чем и как читать результат. Ваши цифры будут своими, и это нормально.
Что снять до миграции
Без базовой линии разговор об «оптимизации» превращается в веру. Зафиксируйте пять показателей на текущем JS-проекте до того, как добавите первый .ts-файл.
- Время холодного билда — сборка с нуля, кэш очищен. Для Webpack/Vite/esbuild это
rm -rf node_modules/.cache distи замер полного прогона. - Время инкрементального билда — изменили один файл, пересобрали. Это то, что разработчик чувствует сотни раз в день.
- Время прогона CI целиком — от
checkoutдо зелёного статуса, усреднённое по 5–10 запускам. - Отзывчивость редактора — задержка автодополнения и «go to definition» в самом большом файле. В VS Code включите
TypeScript > Tsserver: Logлибо смотрите команду Developer: Open Process Explorer и потребление памяти языкового сервиса. - Доля багов, связанных с типами —
undefined is not a function, обращения к несуществующим полям, неверные аргументы. Просмотрите трекер за квартал и посчитайте их вручную.
Последний пункт — единственный, ради которого затевается миграция. Остальные четыре — цена, которую вы за это платите.
Как измерять время сборки честно
Не берите одно значение. Первый прогон греет диск и кэш ОС, JIT ещё не разогрет. Запускайте 5 раз, отбрасывайте первый, берите медиану. Для tsc есть встроенная диагностика:
# TypeScript 5.x: расширенная диагностика компиляции
npx tsc --noEmit --extendedDiagnostics
# ключевые строки в выводе:
# Files: количество обработанных файлов
# Check time: время проверки типов
# Total time: полное время
# трассировка узких мест разрешения типов
npx tsc --noEmit --generateTrace ./trace-outПапку trace-out открывают в chrome://tracing или через @typescript/analyze-trace — видно, какие типы компилятор разбирает дольше всего. Обычно это глубоко вложенные условные типы и большие union.
Что меняется после: типичная картина
После полной миграции показатели расходятся. Одни растут (это накладные расходы), другие падают (это выигрыш). Ниже — направления изменений, которые встречаются в большинстве проектов. Абсолютные значения подставляйте свои.
| Показатель | Направление | Почему |
|---|---|---|
| Холодный билд | Растёт | Добавляется проверка типов. При isolatedModules и транспиляции через esbuild/SWC рост меньше, чем при полном tsc. |
| Инкрементальный билд | Растёт слабо или без изменений | При включённом incremental и файле .tsbuildinfo перепроверяется только затронутое. |
| Прогон CI | Растёт | Добавляется отдельный шаг проверки типов. Его можно распараллелить с линтом и тестами. |
| Отзывчивость редактора | Улучшается субъективно | Автодополнение точнее, но на больших union языковой сервис может подтормаживать. |
| Баги типов в проде | Падает заметно | Класс ошибок «обращение к undefined-полю» ловится компилятором до деплоя. |
Главная ошибка в оценке миграции — сравнивать голыйtscсо старой JS-сборкой. Это сравнение проверки типов с её отсутствием. Разделите транспиляцию и проверку типов: первую отдайте быстрому инструменту, вторую оставьтеtsc --noEmitотдельным шагом.
Где расходы срезаются
Рост времени сборки — не приговор, а следствие того, что один инструмент делает две работы. Разнесите их.
- Транспиляция через esbuild или SWC. Они переводят TS в JS, стирая типы, но не проверяя их. Это в разы быстрее полного
tscс эмиссией. Vite использует esbuild для этого из коробки. - Проверка типов — отдельным процессом. Команда
tsc --noEmitв CI как самостоятельный шаг, параллельно тестам. В проде тип-проверка не нужна вовсе — только на этапе интеграции. - Инкрементальный режим. Флаг
incremental: trueвtsconfig.jsonсоздаёт.tsbuildinfo, и повторные проверки перечитывают только изменённое. В CI этот файл стоит кэшировать между прогонами. - Project references для монорепы. Разбивка на подпроекты с
composite: trueпозволяет проверять типы пакетов независимо и переиспользовать результаты. Собирается черезtsc --build.
// tsconfig.json — базовые флаги, влияющие на скорость
{
"compilerOptions": {
"incremental": true, // переиспользование результатов проверки
"skipLibCheck": true, // не проверять .d.ts зависимостей
"isolatedModules": true, // совместимость с пофайловой транспиляцией
"strict": true
}
}skipLibCheck заметно ускоряет проверку, пропуская типы из node_modules. Плата — компилятор не найдёт конфликт между вашими типами и типами зависимости. На практике для большинства проектов это приемлемый обмен.
Где TypeScript ломается на замерах
Не всё ускоряется, и часть боли всплывает именно после миграции.
- Тяжёлые типы взрывают время проверки. Глубокие условные типы, рекурсивные mapped types и огромные union заставляют компилятор перебирать комбинации.
--generateTraceпокажет, какой именно тип съедает секунды. Иногда проще заменить хитрый generic на явный интерфейс. - Языковой сервис в редакторе тормозит на больших файлах. Автодополнение в файле на несколько тысяч строк с сложными типами может думать секунды. Это не то же, что время билда, — мерится отдельно, через логи tsserver.
- Постепенная миграция с
allowJsзамедляет проверку. Пока в проекте смесь.jsи.ts, компилятор обрабатывает и то, и другое. Полный переход часто быстрее, чем застревание в промежуточном состоянии на полгода. anyобнуляет пользу. Если для скорости миграции всё залитоany, баги типов в проде не упадут — компилятор их не видит. Показатель «баги типов» покажет, что миграция была формальной.
Что мерить в проде после релиза
TypeScript исчезает при сборке — в рантайме исполняется обычный JS. Поэтому в проде мерят не «скорость TS», а косвенные эффекты: изменилась ли частота ошибок класса TypeError: cannot read properties of undefined в системе мониторинга ошибок. Если до миграции таких инцидентов было условно два-три в неделю, а после — единицы в месяц, это и есть окупаемость расходов на билд.
Что делать дальше
Снимите пять базовых показателей до старта — иначе оценить эффект будет нечем. Разнесите транспиляцию и проверку типов сразу, не дожидаясь роста времени CI. Включите incremental и skipLibCheck, закэшируйте .tsbuildinfo в пайплайне. Через месяц после релиза сравните частоту рантайм-ошибок типа undefined в мониторинге — это цифра, которая либо оправдывает миграцию, либо говорит, что вы залили проект any и получили только накладные расходы. Следующий шаг оптимизации — разбор --generateTrace на самых медленных модулях: там прячется большая часть лишних секунд.
Поиск источников: Реальные статьи и книги со ссылками, библиография по ГОСТ
Спросить за 15 ₽Источники: TypeScript: Performance (официальная wiki проекта), TSConfig Reference — compilerOptions
Частые вопросы
У меня Vite, а не Webpack. Замеры отличаются?
Почему после миграции сборка стала медленнее, хотя обещали пользу?
Обязательно ли включать strict сразу?
Что делать, если проверка типов внезапно занимает десятки секунд?
Стоит ли мигрировать постепенно через allowJs?
skipLibCheck безопасен? Не пропущу ли я реальные ошибки?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.