Профилирование TypeScript кода: находим узкие места на практике
Когда ваш TypeScript проект начинает тормозить, а пользователи жалуются на задержки, наступает время профилирования. Профилирование — это не просто "посмотреть, что медленно", а системный подход к выявлению узких мест в производительности кода. В этой статье мы разберем практические методы профилирования TypeScript кода с реальными примерами и инструментами.
Зачем профилировать TypeScript код?
TypeScript компилируется в JavaScript, и все проблемы производительности, присущие JS, переходят в TS. Однако TypeScript добавляет свои нюансы: типизация, декораторы, дженерики могут создавать неожиданные накладные расходы. Профилирование помогает ответить на вопросы:
- Какая функция выполняется дольше всего?
- Где происходит утечка памяти?
- Почему анимация тормозит на 60fps?
- Какой алгоритм требует оптимизации?
Инструменты для профилирования TypeScript
Chrome DevTools (для браузерного кода)
Самый мощный инструмент для фронтенда. Откройте вкладку Performance, нажмите Record, выполните действие и остановите запись. Вы увидите flamegraph — визуализацию вызовов функций и времени их выполнения.
// Пример медленного кода для профилирования
function heavyCalculation(iterations: number): number {
let result = 0;
for (let i = 0; i < iterations; i++) {
result += Math.sqrt(i) * Math.sin(i);
}
return result;
}
// Запустите в консоли браузера, затем профилируйте
heavyCalculation(1000000);
Node.js — встроенный профайлер
Для серверного кода используйте --inspect флаг и Chrome DevTools, или встроенный --prof для генерации логов.
// server.ts
import express from 'express';
const app = express();
app.get('/heavy', (req, res) => {
const data = processData(10000);
res.json({ result: data });
});
function processData(size: number): number[] {
const arr: number[] = [];
for (let i = 0; i < size; i++) {
arr.push(i * Math.random());
}
return arr.sort((a, b) => a - b);
}
app.listen(3000);
// Запуск: node --prof dist/server.js
Benchmark.js для точных замеров
Когда нужно сравнить две реализации, используйте библиотеку benchmark.
import Benchmark from 'benchmark';
const suite = new Benchmark.Suite();
// Медленный вариант
function slowSearch(arr: number[], target: number): boolean {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) return true;
}
return false;
}
// Быстрый вариант с Set
function fastSearch(arr: number[], target: number): boolean {
const set = new Set(arr);
return set.has(target);
}
const testArray = Array.from({ length: 10000 }, (_, i) => i);
suite
.add('slowSearch', () => slowSearch(testArray, 9999))
.add('fastSearch', () => fastSearch(testArray, 9999))
.on('cycle', (event: any) => console.log(String(event.target)))
.on('complete', function(this: any) {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
.run({ async: true });
Практические примеры поиска узких мест
Пример 1: Утечка памяти через замыкания
Типичная проблема в React-приложениях — замыкания, удерживающие ссылки на большие объекты.
// Проблемный код
function createHeavyComponent() {
const hugeData = new Array(1000000).fill('data');
return {
getData: () => hugeData, // замыкание удерживает hugeData
updateData: (newData: string) => {
// мутация, но ссылка остается
hugeData.push(newData);
}
};
}
const component = createHeavyComponent();
// hugeData не будет собран GC, пока существует component
Решение: Используйте WeakMap или явно обнуляйте ссылки.
function createOptimizedComponent() {
let hugeData: string[] | null = new Array(1000000).fill('data');
return {
getData: () => hugeData,
cleanup: () => {
hugeData = null; // разрешаем GC
}
};
}
Пример 2: Медленные циклы с дженериками
TypeScript дженерики на уровне типов не влияют на производительность, но их неправильное использование может привести к неэффективному коду.
// Медленный вариант с рекурсией
function deepFlatten<T>(arr: (T | T[])[]): T[] {
let result: T[] = [];
for (const item of arr) {
if (Array.isArray(item)) {
result = result.concat(deepFlatten(item));
} else {
result.push(item);
}
}
return result;
}
// Быстрый вариант с итеративным подходом
function fastFlatten<T>(arr: (T | T[])[]): T[] {
const result: T[] = [];
const stack = [...arr];
while (stack.length > 0) {
const item = stack.pop()!;
if (Array.isArray(item)) {
stack.push(...item);
} else {
result.push(item);
}
}
return result;
}
// Замер производительности
const nestedArray = [1, [2, [3, [4, 5]]], 6];
console.time('deepFlatten');
for (let i = 0; i < 10000; i++) deepFlatten(nestedArray);
console.timeEnd('deepFlatten'); // ~45ms
console.time('fastFlatten');
for (let i = 0; i < 10000; i++) fastFlatten(nestedArray);
console.timeEnd('fastFlatten'); // ~8ms
Пример 3: Профилирование асинхронного кода
Асинхронные операции часто скрывают узкие места из-за неправильного использования Promise.all или await.
// Медленный последовательный запрос
async function fetchUsersSequential(ids: number[]): Promise<User[]> {
const users: User[] = [];
for (const id of ids) {
const user = await fetch(`/api/users/${id}`).then(r => r.json());
users.push(user);
}
return users;
}
// Быстрый параллельный запрос
async function fetchUsersParallel(ids: number[]): Promise<User[]> {
const promises = ids.map(id =>
fetch(`/api/users/${id}`).then(r => r.json())
);
return Promise.all(promises);
}
// Профилируем с помощью performance.now()
async function profile() {
const ids = [1, 2, 3, 4, 5];
const start1 = performance.now();