7. JavaScript Performance
JavaScript — главная причина медленных сайтов. Даже 50KB нескомпрессированного JS может заморозить браузер на секунды.
Главные враги производительности
Заголовок раздела «Главные враги производительности»1. Длинные задачи (> 50ms) → блокируют main thread2. Memory leaks → растущее потребление памяти3. Synchronous XHR → блокирует UI4. DOM thrashing → множественные reflow5. Лишние re-renders (React) → CPU нагрузкаДлинные задачи (Long Tasks)
Заголовок раздела «Длинные задачи (Long Tasks)»Anything > 50ms на main thread = потенциально некомфортный UX.
// ❌ Длинная задача блокирует UIfunction 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;}Web Workers
Заголовок раздела «Web Workers»Для действительно тяжёлых вычислений — выносим в отдельный поток:
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.jsconst worker = new Worker('./worker.js');
worker.postMessage({ iterations: 10000000 });
worker.addEventListener('message', ({ data }) => { console.log('Result:', data); // UI не был заблокирован!});Memoization
Заголовок раздела «Memoization»// Базовая memoizationfunction 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..." → 25expensiveCalc(5); // Из кэша → 25 (мгновенно!)
// React: useMemo и useCallbackconst 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 и Throttle
Заголовок раздела «Debounce и Throttle»// 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 eventswindow.addEventListener('scroll', throttle(() => { updateScrollProgress(); // максимум раз в 100ms}, 100));Event Delegation
Заголовок раздела «Event Delegation»// ❌ Отдельный 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');Memory Leaks
Заголовок раздела «Memory Leaks»// ❌ Утечка: незакрытый event listenerfunction Component() { useEffect(() => { window.addEventListener('resize', handleResize); // Забыли удалить! }, []);}
// ✅ Очищаем за собойfunction Component() { useEffect(() => { window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); // cleanup! }, []);}
// ❌ Утечка: closure держит большой объектlet largeData = fetchBigData(); // 50MBconst handler = () => { console.log(largeData.id); // closure захватывает всё largeData};
// ✅ Захватываем только нужноеconst { id } = fetchBigData();const handler = () => { console.log(id); // держим только id, largeData может быть GC};