Məzmuna keç
Educora
İrəli22 dəq22 / 27

Performans və yaddaş

Kodu sürətli və qənaətcil et: massiv, `Map` və `Set` əməliyyatlarının Böyük O dəyəri, debounce və throttle, memoizasiya, zibil yığımı və yaddaş sızmaları.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • Massiv, Map və Set əməliyyatlarının Böyük O dəyərini qiymətləndirmək və uyğun verilən strukturunu seçmək
  • Bahalı funksiyaların nə qədər tez-tez işlədiyini debounce, throttle və memoizasiya ilə məhdudlaşdırmaq
  • Zibil yığımını əlçatanlıq baxımından izah etmək və tipik yaddaş sızmalarından qaçmaq

Axtarış xanası hər hərfdə serverə sorğu göndərir — on hərf, on sorğu. 50 000 istifadəçinin siyahısında «bu e-poçt məşğuldurmu?» yoxlaması hər dəfə bütün massivi gəzir. Səhifə ləngiyir, telefon qızır. Performans adətən kiçik fəndlərdən yox, iki şeydən asılıdır: düzgün verilən strukturunu seçmək və artıq iş görməmək.

Böyük O təcrübədə

Böyük O qeydi işin giriş verilənlərinin ölçüsü n artdıqca necə böyüdüyünü göstərir, sabit əmsalları nəzərə almır. O(1) — n-dən asılı deyil; O(log n) — ikili axtarış; O(n) — bir keçid; O(n log n) — yaxşı sıralama; O(n²) — eyni verilənlər üzərində iç-içə dövr. n = 100 000 olanda O(n) yüz min addım, O(n²) isə on milyard addım deməkdir.

ƏməliyyatDəyəri
arr[i], arr.push(x), arr.pop()O(1)
arr.shift(), arr.unshift(x)ümumi halda O(n): elementlər yerini dəyişir
arr.includes(x), indexOf, findO(n)
arr.sort()O(n log n)
map.get/set/has, set.add/has/deleteorta hesabla O(1)
obj[key]orta hesabla O(1)
JavaScript
function hasDuplicatesSlow(items) {
  let steps = 0;
  for (let i = 0; i < items.length; i++) {
    for (let j = i + 1; j < items.length; j++) {
      steps++;
      if (items[i] === items[j]) return { found: true, steps };
    }
  }
  return { found: false, steps };
}

function hasDuplicatesFast(items) {
  const seen = new Set();
  let steps = 0;
  for (const item of items) {
    steps++;
    if (seen.has(item)) return { found: true, steps };
    seen.add(item);
  }
  return { found: false, steps };
}

const ids = Array.from({ length: 2000 }, (_, i) => i);
console.log(hasDuplicatesSlow(ids));
console.log(hasDuplicatesFast(ids));
▸ Gözlənilən nəticə
{ found: false, steps: 1999000 }
{ found: false, steps: 2000 }
2000 element üçün iç-içə dövr təxminən 2 milyon müqayisə aparır, Set isə 2000. Verilənlər 10 dəfə artsa, birinci variant 100 dəfə, ikincisi cəmi 10 dəfə yavaşıyar.

Optimallaşdırmadan əvvəl ölç. performance.now() millisaniyəni kəsr hissəsi ilə verir, DevTools-un Performance paneli isə vaxtın harada getdiyini göstərir. Aşağıdakı kodu işə sal: rəqəmlər hər cihazda fərqli olacaq, amma nisbət aydın görünür.

JavaScript
const n = 20000;
const list = Array.from({ length: n }, (_, i) => i);
const set = new Set(list);

let start = performance.now();
for (let i = 0; i < n; i++) list.includes(i);
console.log('Array.includes:', Math.round(performance.now() - start), 'ms');

start = performance.now();
for (let i = 0; i < n; i++) set.has(i);
console.log('Set.has:', Math.round(performance.now() - start), 'ms');
Nəticə cihazdan asılıdır, ona görə burada gözlənilən çıxış verilməyib.

Debounce və throttle

Bəzi hadisələr çox tez-tez baş verir: input hər hərfdə, scroll və resize isə saniyədə onlarla dəfə. Debounce hadisələr X ms dayanana qədər gözləyir və sonra funksiyanı bir dəfə çağırır — axtarış xanası, avtomatik saxlama. Throttle funksiyanı hər X ms-də ən çoxu bir dəfə çağırır — sürüşdürmə mövqeyi, düyməyə təkrar-təkrar basma.

JavaScript
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

const search = debounce((text) => console.log('Searching for:', text), 200);

search('j');
search('ja');
search('jav');
setTimeout(() => search('javascript'), 500);
▸ Gözlənilən nəticə
Searching for: jav
Searching for: javascript
Dörd çağırışdan yalnız ikisi işlədi: hər «partlayışın» sonuncusu. Hər yeni çağırış köhnə taymeri ləğv edir.
JavaScript
function throttle(fn, interval) {
  let waiting = false;
  return (...args) => {
    if (waiting) return;
    fn(...args);
    waiting = true;
    setTimeout(() => {
      waiting = false;
    }, interval);
  };
}

const handleScroll = throttle((y) => console.log('scroll handled at', y), 100);

[0, 30, 60, 90].forEach((y) => handleScroll(y));
setTimeout(() => [150, 180].forEach((y) => handleScroll(y)), 150);
▸ Gözlənilən nəticə
scroll handled at 0
scroll handled at 150
TexnikaNə vaxt işləyirTipik istifadə
debouncehadisələr dayandıqdan X ms sonra, bir dəfəaxtarış, formanın yoxlanması, avtomatik saxlama
throttlehər X ms-də ən çoxu bir dəfəsürüşdürmə, pəncərənin ölçüsü, oyunda atəş

Memoizasiya

Memoizasiya təmiz funksiyanın nəticələrini arqumentlərə görə yadda saxlamaqdır: eyni arqumentlə ikinci çağırış hazır cavabı dərhal qaytarır. Bu, yaddaş hesabına sürət qazanmaqdır. Yalnız təmiz funksiyalar üçün işləyir, həddi olmayan keş isə sonsuz böyüyə bilər.

JavaScript
function memoize(fn) {
  const cache = new Map();
  return (n) => {
    if (cache.has(n)) return cache.get(n);
    const result = fn(n);
    cache.set(n, result);
    return result;
  };
}

let calls = 0;
const slowFib = (n) => {
  calls++;
  return n < 2 ? n : slowFib(n - 1) + slowFib(n - 2);
};
console.log(slowFib(25), 'calls:', calls);

calls = 0;
const fastFib = memoize((n) => {
  calls++;
  return n < 2 ? n : fastFib(n - 1) + fastFib(n - 2);
});
console.log(fastFib(25), 'calls:', calls);
console.log(fastFib(25), 'calls:', calls);
▸ Gözlənilən nəticə
75025 calls: 242785
75025 calls: 26
75025 calls: 26
Keşsiz variant eyni qiymətləri yüz minlərlə dəfə yenidən hesablayır, memoizasiya ilə hər n yalnız bir dəfə hesablanır.

Zibil yığımı və yaddaş sızmaları

JavaScript yaddaşı özü idarə edir: zibil yığıcı (garbage collector) «köklərdən» — qlobal dəyişənlərdən, cari çağırış stekindən, aktiv qapanmalardan — artıq əlçatan olmayan obyektləri silir. Müasir mühərriklər «işarələ və sil» (mark-and-sweep) üsulundan istifadə edir: köklərdən çatılan hər şey işarələnir, qalanı təmizlənir. Sən zibil yığıcını çağırmırsan — sadəcə lazım olmayan istinadları saxlamamalısan.

  • Unudulmuş taymerlər: dayandırılmayan setInterval qapanmanı və onun bütün verilənlərini yaşadır.
  • Silinmiş elementlərdə qalan hadisə dinləyiciləri və DOM-dan çıxarılsa da dəyişəndə saxlanan elementlər.
  • Yalnız böyüyən qlobal keşlər və massivlər.
JavaScript
const visits = new WeakMap();

function track(user) {
  visits.set(user, (visits.get(user) ?? 0) + 1);
}

let user = { name: 'Elvin' };
track(user);
track(user);
console.log(visits.get(user), visits.has(user));

user = null;

const timer = setInterval(() => console.log('tick'), 1000);
clearInterval(timer);
console.log('Interval cleared: nothing keeps the callback alive');
▸ Gözlənilən nəticə
2 true
Interval cleared: nothing keeps the callback alive
user = null-dan sonra obyekt əlçatmaz olur və zibil yığıcı onu WeakMap-dəki yazısı ilə birlikdə silə bilər — dəqiq anı isə mühərrik seçir. WeakMap-in açarları yalnız obyekt ola bilər və onu gəzmək olmur.
Tapşırıq

memoize(fn) funksiyasını Map ilə yaz ki, slowSquare hər arqument üçün yalnız bir dəfə işləsin. Hazırkı versiya keş saxlamır — ona görə real çağırışların sayı 4-dür, 2 olmalıdır.

Tapşırıq · JavaScript
let calls = 0;
const slowSquare = (n) => {
  calls++;
  return n * n;
};

function memoize(fn) {
  // add a cache
  return (n) => fn(n);
}

const fastSquare = memoize(slowSquare);
console.log(fastSquare(9), fastSquare(9), fastSquare(4), fastSquare(9));
console.log('real calls:', calls);
▸ Gözlənilən nəticə
81 81 16 81
real calls: 2
Tapşırıq

debounce(fn, delay) funksiyasını tamamla: qaytarılan funksiya son çağırışdan delay ms sonra fn-i yalnız bir dəfə çağırmalıdır. İndi o, heç nəyi gözləmir və hər hərfi saxlayır.

Tapşırıq · JavaScript
function debounce(fn, delay) {
  // wait `delay` ms after the last call, then call fn once
  return fn;
}

const save = debounce((text) => console.log('Saved:', text), 300);

save('H');
save('He');
save('Hel');
setTimeout(() => save('Hello'), 100);
setTimeout(() => save('Hello!'), 700);
▸ Gözlənilən nəticə
Saved: Hello
Saved: Hello!

Əsas fikirlər

  • Böyük O işin verilənlərlə necə böyüdüyünü göstərir: massivdə axtarış O(n), Map/Set axtarışı orta hesabla O(1), eyni verilənlər üzərində iç-içə dövr O(n²).
  • Massivdə təkrar-təkrar axtarmaq əvəzinə Set və ya Map-i bir dəfə qur və ondan istifadə et; optimallaşdırmadan əvvəl ölç.
  • Debounce hadisələr dayanandan sonra bir dəfə işləyir, throttle isə hər intervalda ən çoxu bir dəfə.
  • Memoizasiya təmiz funksiyaların nəticələrini keşləyir — yaddaş hesabına sürət.
  • Zibil yığıcı əlçatmaz obyektləri silir; sızmalar unudulmuş taymerlərdən, dinləyicilərdən və böyüyən keşlərdən yaranır, WeakMap isə açarlarını yaşatmır.

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
Hansı axtarış orta hesabla O(1)-dir?