Введение: почему профилирование TypeScript важно для производительности
Когда вы пишете приложение на TypeScript, оно может работать быстро на небольших данных, но с ростом проекта начинают проявляться узкие места — участки кода, которые замедляют выполнение. Профилирование помогает найти эти проблемные места и понять, где тратится больше всего времени.
В этой статье мы разберем, как профилировать TypeScript код с помощью встроенных инструментов браузера и Node.js. Вы узнаете, как находить медленные функции, анализировать стек вызовов и оптимизировать критические участки.
Статья рассчитана на начинающих разработчиков, которые уже знакомы с основами TypeScript и хотят улучшить производительность своих приложений.
Основные понятия профилирования
Профилирование — это процесс сбора данных о том, как программа выполняется: сколько времени занимают функции, как часто они вызываются, какие ресурсы потребляются.
В контексте TypeScript важно понимать несколько ключевых метрик:
- Время выполнения — сколько миллисекунд занимает каждая функция.
- Количество вызовов — как часто вызывается функция.
- Память — сколько памяти выделяется и освобождается.
- Стек вызовов — последовательность вызова функций.
Для профилирования TypeScript кода можно использовать как инструменты браузера (Chrome DevTools), так и утилиты Node.js (например, --inspect или clinic.js).
Инструменты для профилирования TypeScript
Chrome DevTools для клиентского кода
Если ваше TypeScript приложение работает в браузере, лучший инструмент — встроенные Chrome DevTools. Откройте вкладку Performance и нажмите кнопку записи. Выполните действие на странице, остановите запись и получите таймлайн.
Пример анализа: вы видите, что функция processData занимает 300 мс. Это сигнал для оптимизации.
Node.js и встроенный профайлер
Для серверного TypeScript кода (Node.js) используйте флаг --inspect или --prof. Например:
node --inspect dist/app.js
Затем откройте chrome://inspect в браузере и подключитесь к процессу. Вы получите те же возможности DevTools, но для серверного кода.
clinic.js — простая утилита для Node.js
Установите clinic.js глобально:
npm install -g clinic
Запустите профилирование:
clinic doctor -- node dist/app.js
Утилита покажет наглядный отчет с рекомендациями.
Поиск узких мест на практике
Пример 1: Медленная сортировка массива
Предположим, у вас есть функция сортировки большого массива объектов:
interface User {
id: number;
name: string;
age: number;
}
function sortUsers(users: User[]): User[] {
return users.sort((a, b) => {
// Медленное сравнение строк
const nameA = a.name.toLowerCase();
const nameB = b.name.toLowerCase();
if (nameA < nameB) return -1;
if (nameA > nameB) return 1;
return 0;
});
}
При профилировании вы заметите, что toLowerCase() вызывается для каждого сравнения, а их может быть O(n log n). Узкое место — повторное преобразование строк.
Оптимизация: предварительно преобразуйте строки:
function sortUsersOptimized(users: User[]): User[] {
return users
.map(user => ({
...user,
_lowerName: user.name.toLowerCase()
}))
.sort((a, b) => {
if (a._lowerName < b._lowerName) return -1;
if (a._lowerName > b._lowerName) return 1;
return 0;
})
.map(({ _lowerName, ...user }) => user);
}
Пример 2: Частое создание объектов
Рассмотрим функцию, которая обрабатывает массив чисел:
function processNumbers(nums: number[]): number[] {
const result: number[] = [];
for (let i = 0; i < nums.length; i++) {
// Создание нового объекта на каждой итерации
const obj = { value: nums[i], doubled: nums[i] * 2 };
result.push(obj.doubled);
}
return result;
}
Профилирование покажет высокую нагрузку на сборщик мусора из-за создания множества объектов. Узкое место — выделение памяти в цикле.
Оптимизация: избегайте создания объектов в горячих циклах:
function processNumbersOptimized(nums: number[]): number[] {
const result: number[] = new Array(nums.length);
for (let i = 0; i < nums.length; i++) {
result[i] = nums[i] * 2;
}
return result;
}
Пример 3: Сложные рекурсивные вычисления
Рекурсивные функции без мемоизации — классическое узкое место:
function fibonacci(n: number): number {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
Профилирование покажет экспоненциальный рост времени выполнения. Решение — добавить кэширование:
const memo = new Map<number, number>();
function fibonacciMemo(n: number): number {
if (n <= 1) return n;
if (memo.has(n)) return memo.get(n)!;
const result = fibonacciMemo(n - 1) + fibonacciMemo(n - 2);
memo.set(n, result);
return result;
}
Как интерпретировать результаты профилирования
После запуска профилирования вы увидите таблицу с функциями и их временем. Обращайте внимание на:
- Self time — время, потраченное непосредственно в функции (без учета вызовов дочерних функций).
- Total time — общее время с учетом всех вложенных вызовов.
- Call count — количество вызовов. Если функция вызывается миллионы раз, даже микросекунда на вызов дает секунды.
В Chrome DevTools также доступен Flame Chart — графическое представление стека вызовов. Ш