Введение: зачем профилировать 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).
- Минимизация аллокаций: избегайте создания объектов и массивов в горячих циклах.
- Кеширование: сохраняйте результаты дорогих вычислений (мемоизация