Профилирование TypeScript кода: находим узкие места на практике
Каждый разработчик сталкивался с ситуацией, когда приложение начинает тормозить, а причина неочевидна. В мире TypeScript, где код транспилируется в JavaScript, узкие места могут возникать как на этапе выполнения, так и из-за особенностей самой транспиляции. В этой статье мы разберем практические методы профилирования TypeScript кода, которые помогут выявить проблемные участки и повысить производительность вашего приложения.
Почему профилирование TypeScript — это важно?
TypeScript добавляет статическую типизацию, но в рантайме выполняется обычный JavaScript. Это означает, что узкие места могут быть связаны не только с алгоритмами, но и с тем, как TypeScript компилируется. Например, чрезмерное использование декораторов или сложных дженериков может привести к генерации неоптимального кода. Профилирование помогает ответить на вопросы:
- Какие функции выполняются дольше всего?
- Сколько памяти потребляет приложение?
- Где происходят частые сборки мусора?
- Какие участки кода блокируют основной поток?
Без профилирования вы рискуете оптимизировать не те участки, тратя время на микрооптимизации, которые не дадут ощутимого прироста.
Инструменты для профилирования TypeScript
Для профилирования TypeScript кода мы будем использовать три основных инструмента, которые покрывают большинство сценариев: от браузерных приложений до серверных решений на Node.js.
1. Chrome DevTools для браузерных приложений
Самый доступный и мощный инструмент. Откройте вкладку Performance в Chrome DevTools, запустите запись и выполните нужное действие в приложении. После остановки вы получите flame chart — визуальное представление вызовов функций. Вот как это выглядит на практике:
// Пример медленного кода на TypeScript
function processData(data: number[]): number[] {
const result: number[] = [];
for (let i = 0; i < data.length; i++) {
// Потенциально узкое место: частые вызовы
result.push(data[i] * Math.sqrt(data[i]));
}
return result;
}
// Использование
const largeArray = Array.from({ length: 100000 }, (_, i) => i);
console.time('processData');
processData(largeArray);
console.timeEnd('processData'); // ~15ms
В Chrome DevTools вы увидите, что функция processData занимает значительное время, а внутри нее — множество вызовов Math.sqrt. Это подсказка, что можно заменить итерацию на map или векторизовать вычисления.
2. Node.js profiler для серверных приложений
Для Node.js есть встроенный профайлер. Запустите приложение с флагом --prof:
node --prof dist/app.js
После завершения работы сгенерируется файл isolate-*.log. Преобразуйте его в читаемый формат:
node --prof-process isolate-*.log > processed.txt
Пример типичной проблемы — частые аллокации в циклах:
// Плохой пример: создание объектов в цикле
interface User {
id: number;
name: string;
}
function createUsers(ids: number[]): User[] {
const users: User[] = [];
for (const id of ids) {
users.push({ id, name: `User_${id}` }); // Новая аллокация
}
return users;
}
Профайлер покажет, что функция createUsers создает много временных объектов, что увеличивает нагрузку на сборщик мусора. Оптимизация: заранее выделить память или использовать пул объектов.
3. Benchmark.js для микробенчмарков
Когда нужно сравнить производительность двух реализаций, используйте библиотеку benchmark.js. Установите ее через npm:
npm install benchmark @types/benchmark
Пример сравнения двух способов фильтрации массива:
import Benchmark from 'benchmark';
const suite = new Benchmark.Suite();
const data = Array.from({ length: 1000 }, (_, i) => i);
suite
.add('filter + includes', () => {
const filtered = data.filter(x => x % 2 === 0);
filtered.includes(500);
})
.add('for loop', () => {
const filtered: number[] = [];
for (let i = 0; i < data.length; i++) {
if (data[i] % 2 === 0) filtered.push(data[i]);
}
filtered.includes(500);
})
.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 });
Результат покажет, что for loop часто быстрее, чем filter, из-за отсутствия дополнительного вызова функции.
Практический пример: поиск узкого места в сложном алгоритме
Рассмотрим реальный сценарий — приложение для обработки изображений на TypeScript. Допустим, у нас есть функция для применения фильтра к пикселям:
interface Pixel {
r: number;
g: number;
b: number;
}
function applyFilter(image: Pixel[][]): Pixel[][] {
const height = image.length;
const width = image[0].length;
const result: Pixel[][] = [];
for (let y = 0; y < height; y++) {
result[y] = [];
for (let x = 0; x < width; x++) {
const pixel = image[y][x];
// Узкое место: преобразование в градации серого
const gray = Math.round(0.299 * pixel.r + 0.587 * pixel.g + 0.114 * pixel.b);
result[y][x] = { r: gray, g: gray, b: gray };
}
}
return result;
}
Запустив профилирование через Node.js, мы видим, что функция выполняется 200ms для изображения 1000x1000. Анализ flame chart показывает, что 80% времени тратится на вычисление Math.round и умножение с плавающей точкой. Оптимизация: предварительно вычислить коэффициенты и использовать целочисленную арифметику:
// Оптимизированная версия
const R_COEFF = 0.299 * 256;
const G_COEFF = 0.587 * 256;
const B_COEFF = 0.114 * 256;
function applyFilterOptimized(image: Pixel[][]): Pixel[][] {
const height = image.length;
const width