Перейти к содержимому

7. JavaScript Performance

JavaScript — главная причина медленных сайтов. Даже 50KB нескомпрессированного JS может заморозить браузер на секунды.

1. Длинные задачи (> 50ms) → блокируют main thread
2. Memory leaks → растущее потребление памяти
3. Synchronous XHR → блокирует UI
4. DOM thrashing → множественные reflow
5. Лишние re-renders (React) → CPU нагрузка

Anything > 50ms на main thread = потенциально некомфортный UX.

// ❌ Длинная задача блокирует UI
function processLargeData(data) {
return data.map(item => heavyCompute(item)); // занимает 500ms
}
// ✅ Разбиваем на чанки
async function processInChunks(data, chunkSize = 100) {
const results = [];
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
results.push(...chunk.map(item => heavyCompute(item)));
// Даём браузеру "подышать"
await new Promise(resolve => setTimeout(resolve, 0));
}
return results;
}
// ✅ Scheduler API (Chrome 94+)
async function processWithScheduler(data) {
const results = [];
for (const item of data) {
if (navigator.scheduling?.isInputPending()) {
await scheduler.yield(); // уступаем input events
}
results.push(heavyCompute(item));
}
return results;
}

Для действительно тяжёлых вычислений — выносим в отдельный поток:

worker.js
self.addEventListener('message', ({ data }) => {
const result = heavyComputation(data);
self.postMessage(result);
});
function heavyComputation(input) {
// CPU-интенсивная работа не блокирует UI!
let result = 0;
for (let i = 0; i < input.iterations; i++) {
result += Math.sqrt(i);
}
return result;
}
// main.js
const worker = new Worker('./worker.js');
worker.postMessage({ iterations: 10000000 });
worker.addEventListener('message', ({ data }) => {
console.log('Result:', data); // UI не был заблокирован!
});
// Базовая memoization
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Использование
const expensiveCalc = memoize((n) => {
console.log('Computing...');
return n * n;
});
expensiveCalc(5); // "Computing..." → 25
expensiveCalc(5); // Из кэша → 25 (мгновенно!)
// React: useMemo и useCallback
const Component = ({ data, onSelect }) => {
// Мемоизируем тяжёлые вычисления
const processedData = useMemo(() => {
return data.map(item => heavyTransform(item));
}, [data]); // пересчитываем только когда data изменился
// Мемоизируем функции
const handleSelect = useCallback((id) => {
onSelect(id);
}, [onSelect]); // не пересоздаём на каждый render
return <List data={processedData} onSelect={handleSelect} />;
};
// Debounce — откладывает выполнение до паузы
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// Использование: поиск при вводе
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(async (e) => {
const results = await fetch(`/api/search?q=${e.target.value}`);
// Запрос отправится только через 300ms после паузы
}, 300));
// Throttle — выполняет не чаще раза в N мс
function throttle(fn, limit) {
let lastRun = 0;
return function(...args) {
const now = Date.now();
if (now - lastRun >= limit) {
lastRun = now;
return fn.apply(this, args);
}
};
}
// Использование: scroll events
window.addEventListener('scroll', throttle(() => {
updateScrollProgress(); // максимум раз в 100ms
}, 100));
// ❌ Отдельный listener на каждый элемент (1000 listeners!)
document.querySelectorAll('.button').forEach(btn => {
btn.addEventListener('click', handler);
});
// ✅ Один listener на родителя
document.getElementById('list').addEventListener('click', (e) => {
const button = e.target.closest('.button');
if (button) {
handler(button);
}
});
// ❌ Обращение к .length на каждой итерации
for (let i = 0; i < array.length; i++) {
process(array[i]);
}
// ✅ Кэшируем длину
for (let i = 0, len = array.length; i < len; i++) {
process(array[i]);
}
// ✅ Для трансформаций — функциональный стиль (JIT-оптимизируется)
const results = array.map(item => process(item));
// ✅ Для больших массивов — прямой цикл быстрее
const results = new Array(array.length);
for (let i = 0, len = array.length; i < len; i++) {
results[i] = process(array[i]);
}
// Бенчмаркинг
console.time('loop');
// ... код
console.timeEnd('loop');
// ❌ Утечка: незакрытый event listener
function Component() {
useEffect(() => {
window.addEventListener('resize', handleResize);
// Забыли удалить!
}, []);
}
// ✅ Очищаем за собой
function Component() {
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize); // cleanup!
}, []);
}
// ❌ Утечка: closure держит большой объект
let largeData = fetchBigData(); // 50MB
const handler = () => {
console.log(largeData.id); // closure захватывает всё largeData
};
// ✅ Захватываем только нужное
const { id } = fetchBigData();
const handler = () => {
console.log(id); // держим только id, largeData может быть GC
};