Профилирование TypeScript кода: находим узкие места

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: зачем профилировать TypeScript?

TypeScript — мощный инструмент для разработки масштабируемых приложений, но даже самый хорошо типизированный код может страдать от скрытых проблем производительности. Узкие места (bottlenecks) возникают из-за неэффективных алгоритмов, избыточных операций ввода-вывода, неправильного управления памятью или медленных вычислений. Профилирование позволяет выявить эти участки и целенаправленно их оптимизировать.

В этой статье мы рассмотрим практические методы профилирования TypeScript-кода: от встроенных инструментов браузера до специализированных утилит для Node.js. Вы научитесь находить медленные функции, анализировать стек вызовов и принимать решения на основе данных.

Инструменты профилирования TypeScript

Профилирование TypeScript-кода не отличается от профилирования JavaScript, так как TypeScript компилируется в JS. Однако важно учитывать, что исходный код на TypeScript может содержать дополнительные абстракции (дженерики, декораторы, интерфейсы), которые после компиляции могут порождать неочевидные паттерны.

1. Chrome DevTools Performance

Для фронтенд-приложений лучший выбор — вкладка Performance в Chrome DevTools. Она записывает активность страницы и показывает фрейм-график, время выполнения функций, события и использование памяти.

Как использовать:

  • Откройте DevTools (F12) → вкладка Performance.
  • Нажмите кнопку записи (кружок), выполните нужное действие на странице.
  • Остановите запись и изучите flame chart.

Пример: если у вас есть функция обработки массива, которая выполняется дольше 100 мс, вы увидите её в виде широкого блока на графике. Кликните на неё — DevTools покажет исходный код (если source maps подключены) и время выполнения.

// Исходный TypeScript код
function processItems(items: number[]): number[] {
  return items
    .filter(x => x > 10)
    .map(x => x * 2)
    .reduce((acc, val) => acc + val, 0);
}

После профилирования вы можете увидеть, что filter и map создают промежуточные массивы, что замедляет работу на больших данных. Оптимизация — заменить цепочку на один проход с for или reduce.

2. Node.js встроенный профайлер

Для серверного кода на Node.js используйте встроенный профайлер V8. Он собирает сэмплы стека вызовов и позволяет анализировать, какие функции занимают больше всего времени.

Запуск:

node --prof your-compiled-file.js

После выполнения появится файл isolate-0x...-v8.log. Преобразуйте его в читаемый формат:

node --prof-process isolate-0x...-v8.log > profile.txt

В полученном файле вы увидите таблицу с колонками: ticks (количество сэмплов), total (общее время), nonlib (время без учёта библиотек) и имя функции. Функции с наибольшим количеством ticks — ваши кандидаты на оптимизацию.

3. ts-node и source maps

При разработке на TypeScript часто используют ts-node для запуска кода без предварительной компиляции. Для профилирования убедитесь, что source maps включены в tsconfig.json:

{
  "compilerOptions": {
    "sourceMap": true,
    "inlineSourceMap": true
  }
}

Затем запустите приложение с флагом --inspect:

node --inspect -r ts-node/register src/app.ts

Откройте Chrome и перейдите по адресу chrome://inspect. Вы увидите ваше приложение и сможете использовать DevTools для профилирования, видя исходный TypeScript-код, а не скомпилированный JavaScript.

Поиск узких мест: практические примеры

Рассмотрим три типичных сценария, где профилирование помогает найти проблемы.

1. Медленные вычисления в цикле

Предположим, у вас есть функция, которая обрабатывает массив объектов с вложенными данными:

interface User {
  id: number;
  name: string;
  scores: number[];
}

function calculateAverageScores(users: User[]): number[] {
  return users.map(user => {
    const total = user.scores.reduce((a, b) => a + b, 0);
    return total / user.scores.length;
  });
}

Профилирование показывает, что reduce вызывается для каждого пользователя, создавая новый массив результатов. Если пользователей миллион, это может быть узким местом. Оптимизация — использовать простой цикл for и избегать создания промежуточных массивов.

2. Избыточные вызовы функций

Часто разработчики вызывают функции внутри шаблонов или вычисляемых свойств, не подозревая, что они выполняются многократно. Например, в React компоненте:

function ExpensiveComponent({ data }: { data: number[] }) {
  const computed = data.map(x => heavyComputation(x)); // выполняется при каждом рендере
  return <div>{computed.join(', ')}</div>;
}

Профилирование DevTools покажет, что heavyComputation вызывается при каждом обновлении. Решение — использовать useMemo или вынести вычисления за пределы компонента.

3. Утечки памяти

Утечки памяти сложно обнаружить без профилирования. Используйте вкладку Memory в DevTools или Node.js --inspect с heap snapshot. Если после серии операций размер кучи не уменьшается (при сборке мусора), значит, есть объекты, которые не освобождаются. Типичные причины — забытые подписки на события, замыкания или глобальные переменные.

Анализ результатов и оптимизация

После того как вы нашли узкие места, приступайте к оптимизации. Придерживайтесь правила: измеряйте, оптимизируйте, измеряйте снова.

  • Алгоритмическая оптимизация: замените O(n²) на O(n log n) или O(n).
  • Минимизация аллокаций: избегайте создания объектов и массивов в горячих циклах.
  • Кеширование: сохраняйте результаты дорогих вычислений (мемоизация

Блоги

Рекомендаций по книгам