← Собеседование/Оптимизация производительности JavaScript#376 из 383← ПредыдущийСледующий →+40 XP
Полезно по теме:Маршрут: подготовка к интервьюГайд: портфолио juniorГайд: карьерный планТермин: Closure
← НазадДалее →

Оптимизация производительности JavaScript

Краткий ответ

Оптимизация JS-кода охватывает несколько уровней: предотвращение утечек памяти, избегание layout thrashing в браузере, использование мемоизации для дорогих вычислений, debounce/throttle для частых событий, Web Workers для CPU-интенсивных задач. V8 лучше оптимизирует предсказуемый код — не меняй форму объектов и типы переменных после создания.

Полный разбор

Утечки памяти

// УТЕЧКА 1: Event listener без cleanup
class Timer {
  start() {
    // Каждый вызов start() добавляет новый listener
    window.addEventListener('resize', this.handleResize)
  }
  // Нет метода stop() с removeEventListener!
}

// ИСПРАВЛЕНИЕ:
class Timer {
  constructor() {
    this.handleResize = this.handleResize.bind(this)
  }
  start() {
    window.addEventListener('resize', this.handleResize)
  }
  stop() {
    window.removeEventListener('resize', this.handleResize)
  }
}

// УТЕЧКА 2: Замыкание держит ссылку на большой объект
function createLeak() {
  const hugeData = new Array(1000000).fill('data')

  return function() {
    // hugeData никогда не будет собран GC
    // даже если мы не используем его внутри
    console.log('done')
  }
}

// ИСПРАВЛЕНИЕ: убираем ссылку
function noLeak() {
  let hugeData = new Array(1000000).fill('data')
  const result = processData(hugeData)
  hugeData = null  // позволяем GC собрать данные

  return function() {
    console.log(result)
  }
}

// УТЕЧКА 3: Unbounded cache (кэш без лимита)
const cache = {}  // растёт бесконечно!

// ИСПРАВЛЕНИЕ: WeakMap (автоочистка) или LRU-кэш с лимитом
const weakCache = new WeakMap()  // ключи-объекты освобождаются автоматически

Layout Thrashing (принудительный синхронный layout)

// ПЛОХО: чтение и запись DOM вперемешку — каждое чтение форсирует reflow
function badLayout(elements) {
  elements.forEach(el => {
    const height = el.offsetHeight  // READ — forceлayout reflow
    el.style.height = height * 2 + 'px'  // WRITE
    // Следующая итерация: снова READ после WRITE — браузер обязан пересчитать!
  })
}

// ХОРОШО: сначала все reads, потом все writes (batching)
function goodLayout(elements) {
  // Фаза чтения
  const heights = elements.map(el => el.offsetHeight)

  // Фаза записи (браузер может батчить)
  elements.forEach((el, i) => {
    el.style.height = heights[i] * 2 + 'px'
  })
}

requestAnimationFrame для анимаций

// НЕ используй setInterval для анимаций
setInterval(() => {
  element.style.left = (x++) + 'px'
}, 16)  // может не совпасть с частотой экрана, вызывает jank

// requestAnimationFrame — синхронизирован с частотой обновления экрана
function animate() {
  element.style.left = (x++) + 'px'

  if (x < 100) {
    requestAnimationFrame(animate)  // следующий кадр
  }
}
requestAnimationFrame(animate)

Мемоизация

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 fib = memoize(function(n) {
  if (n <= 1) return n
  return fib(n - 1) + fib(n - 2)
})

// fib(40): без мемоизации ~1 млрд вызовов, с мемоизацией — 40

V8-оптимизации: мономорфизм

// ПЛОХО: V8 не может оптимизировать полиморфный код
function process(obj) {
  return obj.value * 2
}

process({ value: 1, name: 'a' })     // форма 1
process({ value: 2, extra: true })   // форма 2 — V8 деоптимизирует!

// ХОРОШО: всегда одна форма объекта
class Point {
  constructor(x, y) {
    this.x = x
    this.y = y
  }
}
// Все Point имеют одинаковую форму — V8 генерирует оптимальный машинный код

// ПЛОХО: изменение типа переменной
let x = 42      // integer
x = 'hello'     // string — V8 деоптимизирует переменную!

// ХОРОШО: стабильные типы
const count = 42
const text = 'hello'

Web Workers для CPU-интенсивных задач

// Главный поток (main.js)
// Тяжёлые вычисления блокируют UI — используй Worker
const worker = new Worker('worker.js')

worker.postMessage({ data: largeArray })

worker.onmessage = (event) => {
  console.log('Результат из Worker:', event.data.result)
}

// worker.js — выполняется в отдельном потоке
self.onmessage = (event) => {
  const { data } = event.data
  const result = heavyComputation(data)  // не блокирует UI
  self.postMessage({ result })
}

Связанные уроки курса

  • Замыкания — механизм, через который возникают утечки памяти и через который работает мемоизация
  • Рекурсия — мемоизация особенно важна для рекурсивных алгоритмов
  • requestAnimationFrame — детальный разбор анимаций в JS
  • WeakRef и FinalizationRegistry — слабые ссылки для избежания утечек памяти
  • Как отвечать на собеседовании

    Структурируй ответ по категориям: память, рендеринг, вычисления. Приведи конкретные примеры утечек (event listeners, замыкания). Обязательно упомяни мемоизацию и объясни, где она применима (чистые функции, дорогие вычисления). Упомяни requestAnimationFrame вместо setInterval для анимаций. Если спрашивают про большие объёмы данных — скажи про Web Workers.

    Красные флаги ответа

  • «Просто используй усё меньше циклов» без конкретики — интервьюер ожидает системного понимания, а не расплывчатых советов
  • Незнание про layout thrashing — частая причина производительных проблем в браузерных приложениях, базовая тема для frontend-разработчика
  • Мемоизация без упоминания ограничений — кэш без лимита сам является утечкой памяти, нужно знать о LRU и ограничении размера
  • Примеры

    Мемоизация с лимитом кэша, демонстрация утечки памяти и исправление, замер производительности

    // ===== МЕМОИЗАЦИЯ =====
    console.log('=== Мемоизация ===')
    
    function memoize(fn, maxSize = 100) {
      const cache = new Map()
    
      return function(...args) {
        const key = JSON.stringify(args)
    
        if (cache.has(key)) {
          console.log('Кэш hit:', key)
          return cache.get(key)
        }
    
        // LRU: если кэш полон — удаляем самый старый элемент
        if (cache.size >= maxSize) {
          const firstKey = cache.keys().next().value
          cache.delete(firstKey)
        }
    
        const result = fn.apply(this, args)
        cache.set(key, result)
        console.log('Кэш miss, вычислено:', result)
        return result
      }
    }
    
    // Тяжёлая функция — Фибоначчи
    function slowFib(n) {
      if (n <= 1) return n
      return slowFib(n - 1) + slowFib(n - 2)
    }
    
    // Мемоизированная версия
    const fastFib = memoize(function fib(n) {
      if (n <= 1) return n
      return fastFib(n - 1) + fastFib(n - 2)
    })
    
    console.log('\nfib(10):')
    console.log(fastFib(10))  // вычисляет
    
    console.log('\nfib(10) снова:')
    console.log(fastFib(10))  // из кэша
    
    // Замер разницы в производительности
    console.log('\n=== Замер производительности ===')
    
    let t0, t1
    
    // БЕЗ мемоизации
    t0 = Date.now()
    const resultSlow = slowFib(35)
    t1 = Date.now()
    console.log('slowFib(35):', resultSlow, '| Время:', t1 - t0, 'ms')
    
    // С мемоизацией
    const memoFib = memoize(function fibM(n) {
      if (n <= 1) return n
      return memoFib(n - 1) + memoFib(n - 2)
    })
    
    t0 = Date.now()
    const resultFast = memoFib(35)
    t1 = Date.now()
    console.log('memoFib(35):', resultFast, '| Время:', t1 - t0, 'ms')
    
    // ===== ПАТТЕРН УТЕЧКИ ПАМЯТИ И ИСПРАВЛЕНИЕ =====
    console.log('\n=== Утечка памяти: пример и исправление ===')
    
    // УТЕЧКА: unbounded cache
    function createLeakyService() {
      const requestCache = {}  // никогда не очищается!
    
      return {
        getData(key) {
          if (!requestCache[key]) {
            requestCache[key] = { data: new Array(1000).fill(key), timestamp: Date.now() }
          }
          return requestCache[key]
        },
        // нет метода для очистки кэша!
        getCacheSize() {
          return Object.keys(requestCache).length
        }
      }
    }
    
    // ИСПРАВЛЕНИЕ: кэш с TTL и лимитом
    function createSafeService(maxItems = 50, ttlMs = 60000) {
      const cache = new Map()
    
      function cleanup() {
        const now = Date.now()
        for (const [key, entry] of cache.entries()) {
          if (now - entry.timestamp > ttlMs) {
            cache.delete(key)
          }
        }
        // Также соблюдаем лимит по размеру (LRU)
        while (cache.size > maxItems) {
          const oldestKey = cache.keys().next().value
          cache.delete(oldestKey)
        }
      }
    
      return {
        getData(key) {
          cleanup()  // очищаем устаревшие записи
    
          if (cache.has(key)) {
            return cache.get(key).data
          }
    
          const data = { value: key, computed: Date.now() }
          cache.set(key, { data, timestamp: Date.now() })
          return data
        },
        getCacheSize() {
          return cache.size
        }
      }
    }
    
    const leaky = createLeakyService()
    const safe  = createSafeService(3)  // лимит 3 для демо
    
    // Симулируем много запросов
    for (let i = 0; i < 10; i++) {
      leaky.getData(`key-${i}`)
      safe.getData(`key-${i}`)
    }
    
    console.log('Leaky кэш размер:', leaky.getCacheSize())  // 10 (растёт)
    console.log('Safe кэш размер:', safe.getCacheSize())    // 3 (ограничен)
    
    // ===== МЕМОИЗАЦИЯ С РАЗНЫМИ ТИПАМИ АРГУМЕНТОВ =====
    console.log('\n=== Мемоизация сложных аргументов ===')
    
    const memoizeDeep = memoize((arr, multiplier) => {
      console.log('  Вычисление...')
      return arr.map(x => x * multiplier)
    })
    
    console.log(memoizeDeep([1, 2, 3], 2))  // вычисляет
    console.log(memoizeDeep([1, 2, 3], 2))  // из кэша
    console.log(memoizeDeep([1, 2, 3], 3))  // новые аргументы — вычисляет

    Оптимизация производительности JavaScript

    Краткий ответ

    Оптимизация JS-кода охватывает несколько уровней: предотвращение утечек памяти, избегание layout thrashing в браузере, использование мемоизации для дорогих вычислений, debounce/throttle для частых событий, Web Workers для CPU-интенсивных задач. V8 лучше оптимизирует предсказуемый код — не меняй форму объектов и типы переменных после создания.

    Полный разбор

    Утечки памяти

    // УТЕЧКА 1: Event listener без cleanup
    class Timer {
      start() {
        // Каждый вызов start() добавляет новый listener
        window.addEventListener('resize', this.handleResize)
      }
      // Нет метода stop() с removeEventListener!
    }
    
    // ИСПРАВЛЕНИЕ:
    class Timer {
      constructor() {
        this.handleResize = this.handleResize.bind(this)
      }
      start() {
        window.addEventListener('resize', this.handleResize)
      }
      stop() {
        window.removeEventListener('resize', this.handleResize)
      }
    }
    
    // УТЕЧКА 2: Замыкание держит ссылку на большой объект
    function createLeak() {
      const hugeData = new Array(1000000).fill('data')
    
      return function() {
        // hugeData никогда не будет собран GC
        // даже если мы не используем его внутри
        console.log('done')
      }
    }
    
    // ИСПРАВЛЕНИЕ: убираем ссылку
    function noLeak() {
      let hugeData = new Array(1000000).fill('data')
      const result = processData(hugeData)
      hugeData = null  // позволяем GC собрать данные
    
      return function() {
        console.log(result)
      }
    }
    
    // УТЕЧКА 3: Unbounded cache (кэш без лимита)
    const cache = {}  // растёт бесконечно!
    
    // ИСПРАВЛЕНИЕ: WeakMap (автоочистка) или LRU-кэш с лимитом
    const weakCache = new WeakMap()  // ключи-объекты освобождаются автоматически

    Layout Thrashing (принудительный синхронный layout)

    // ПЛОХО: чтение и запись DOM вперемешку — каждое чтение форсирует reflow
    function badLayout(elements) {
      elements.forEach(el => {
        const height = el.offsetHeight  // READ — forceлayout reflow
        el.style.height = height * 2 + 'px'  // WRITE
        // Следующая итерация: снова READ после WRITE — браузер обязан пересчитать!
      })
    }
    
    // ХОРОШО: сначала все reads, потом все writes (batching)
    function goodLayout(elements) {
      // Фаза чтения
      const heights = elements.map(el => el.offsetHeight)
    
      // Фаза записи (браузер может батчить)
      elements.forEach((el, i) => {
        el.style.height = heights[i] * 2 + 'px'
      })
    }

    requestAnimationFrame для анимаций

    // НЕ используй setInterval для анимаций
    setInterval(() => {
      element.style.left = (x++) + 'px'
    }, 16)  // может не совпасть с частотой экрана, вызывает jank
    
    // requestAnimationFrame — синхронизирован с частотой обновления экрана
    function animate() {
      element.style.left = (x++) + 'px'
    
      if (x < 100) {
        requestAnimationFrame(animate)  // следующий кадр
      }
    }
    requestAnimationFrame(animate)

    Мемоизация

    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 fib = memoize(function(n) {
      if (n <= 1) return n
      return fib(n - 1) + fib(n - 2)
    })
    
    // fib(40): без мемоизации ~1 млрд вызовов, с мемоизацией — 40

    V8-оптимизации: мономорфизм

    // ПЛОХО: V8 не может оптимизировать полиморфный код
    function process(obj) {
      return obj.value * 2
    }
    
    process({ value: 1, name: 'a' })     // форма 1
    process({ value: 2, extra: true })   // форма 2 — V8 деоптимизирует!
    
    // ХОРОШО: всегда одна форма объекта
    class Point {
      constructor(x, y) {
        this.x = x
        this.y = y
      }
    }
    // Все Point имеют одинаковую форму — V8 генерирует оптимальный машинный код
    
    // ПЛОХО: изменение типа переменной
    let x = 42      // integer
    x = 'hello'     // string — V8 деоптимизирует переменную!
    
    // ХОРОШО: стабильные типы
    const count = 42
    const text = 'hello'

    Web Workers для CPU-интенсивных задач

    // Главный поток (main.js)
    // Тяжёлые вычисления блокируют UI — используй Worker
    const worker = new Worker('worker.js')
    
    worker.postMessage({ data: largeArray })
    
    worker.onmessage = (event) => {
      console.log('Результат из Worker:', event.data.result)
    }
    
    // worker.js — выполняется в отдельном потоке
    self.onmessage = (event) => {
      const { data } = event.data
      const result = heavyComputation(data)  // не блокирует UI
      self.postMessage({ result })
    }

    Связанные уроки курса

  • Замыкания — механизм, через который возникают утечки памяти и через который работает мемоизация
  • Рекурсия — мемоизация особенно важна для рекурсивных алгоритмов
  • requestAnimationFrame — детальный разбор анимаций в JS
  • WeakRef и FinalizationRegistry — слабые ссылки для избежания утечек памяти
  • Как отвечать на собеседовании

    Структурируй ответ по категориям: память, рендеринг, вычисления. Приведи конкретные примеры утечек (event listeners, замыкания). Обязательно упомяни мемоизацию и объясни, где она применима (чистые функции, дорогие вычисления). Упомяни requestAnimationFrame вместо setInterval для анимаций. Если спрашивают про большие объёмы данных — скажи про Web Workers.

    Красные флаги ответа

  • «Просто используй усё меньше циклов» без конкретики — интервьюер ожидает системного понимания, а не расплывчатых советов
  • Незнание про layout thrashing — частая причина производительных проблем в браузерных приложениях, базовая тема для frontend-разработчика
  • Мемоизация без упоминания ограничений — кэш без лимита сам является утечкой памяти, нужно знать о LRU и ограничении размера
  • Примеры

    Мемоизация с лимитом кэша, демонстрация утечки памяти и исправление, замер производительности

    // ===== МЕМОИЗАЦИЯ =====
    console.log('=== Мемоизация ===')
    
    function memoize(fn, maxSize = 100) {
      const cache = new Map()
    
      return function(...args) {
        const key = JSON.stringify(args)
    
        if (cache.has(key)) {
          console.log('Кэш hit:', key)
          return cache.get(key)
        }
    
        // LRU: если кэш полон — удаляем самый старый элемент
        if (cache.size >= maxSize) {
          const firstKey = cache.keys().next().value
          cache.delete(firstKey)
        }
    
        const result = fn.apply(this, args)
        cache.set(key, result)
        console.log('Кэш miss, вычислено:', result)
        return result
      }
    }
    
    // Тяжёлая функция — Фибоначчи
    function slowFib(n) {
      if (n <= 1) return n
      return slowFib(n - 1) + slowFib(n - 2)
    }
    
    // Мемоизированная версия
    const fastFib = memoize(function fib(n) {
      if (n <= 1) return n
      return fastFib(n - 1) + fastFib(n - 2)
    })
    
    console.log('\nfib(10):')
    console.log(fastFib(10))  // вычисляет
    
    console.log('\nfib(10) снова:')
    console.log(fastFib(10))  // из кэша
    
    // Замер разницы в производительности
    console.log('\n=== Замер производительности ===')
    
    let t0, t1
    
    // БЕЗ мемоизации
    t0 = Date.now()
    const resultSlow = slowFib(35)
    t1 = Date.now()
    console.log('slowFib(35):', resultSlow, '| Время:', t1 - t0, 'ms')
    
    // С мемоизацией
    const memoFib = memoize(function fibM(n) {
      if (n <= 1) return n
      return memoFib(n - 1) + memoFib(n - 2)
    })
    
    t0 = Date.now()
    const resultFast = memoFib(35)
    t1 = Date.now()
    console.log('memoFib(35):', resultFast, '| Время:', t1 - t0, 'ms')
    
    // ===== ПАТТЕРН УТЕЧКИ ПАМЯТИ И ИСПРАВЛЕНИЕ =====
    console.log('\n=== Утечка памяти: пример и исправление ===')
    
    // УТЕЧКА: unbounded cache
    function createLeakyService() {
      const requestCache = {}  // никогда не очищается!
    
      return {
        getData(key) {
          if (!requestCache[key]) {
            requestCache[key] = { data: new Array(1000).fill(key), timestamp: Date.now() }
          }
          return requestCache[key]
        },
        // нет метода для очистки кэша!
        getCacheSize() {
          return Object.keys(requestCache).length
        }
      }
    }
    
    // ИСПРАВЛЕНИЕ: кэш с TTL и лимитом
    function createSafeService(maxItems = 50, ttlMs = 60000) {
      const cache = new Map()
    
      function cleanup() {
        const now = Date.now()
        for (const [key, entry] of cache.entries()) {
          if (now - entry.timestamp > ttlMs) {
            cache.delete(key)
          }
        }
        // Также соблюдаем лимит по размеру (LRU)
        while (cache.size > maxItems) {
          const oldestKey = cache.keys().next().value
          cache.delete(oldestKey)
        }
      }
    
      return {
        getData(key) {
          cleanup()  // очищаем устаревшие записи
    
          if (cache.has(key)) {
            return cache.get(key).data
          }
    
          const data = { value: key, computed: Date.now() }
          cache.set(key, { data, timestamp: Date.now() })
          return data
        },
        getCacheSize() {
          return cache.size
        }
      }
    }
    
    const leaky = createLeakyService()
    const safe  = createSafeService(3)  // лимит 3 для демо
    
    // Симулируем много запросов
    for (let i = 0; i < 10; i++) {
      leaky.getData(`key-${i}`)
      safe.getData(`key-${i}`)
    }
    
    console.log('Leaky кэш размер:', leaky.getCacheSize())  // 10 (растёт)
    console.log('Safe кэш размер:', safe.getCacheSize())    // 3 (ограничен)
    
    // ===== МЕМОИЗАЦИЯ С РАЗНЫМИ ТИПАМИ АРГУМЕНТОВ =====
    console.log('\n=== Мемоизация сложных аргументов ===')
    
    const memoizeDeep = memoize((arr, multiplier) => {
      console.log('  Вычисление...')
      return arr.map(x => x * multiplier)
    })
    
    console.log(memoizeDeep([1, 2, 3], 2))  // вычисляет
    console.log(memoizeDeep([1, 2, 3], 2))  // из кэша
    console.log(memoizeDeep([1, 2, 3], 3))  // новые аргументы — вычисляет

    Задание

    Реализуй функцию memoize(fn, maxSize), которая кэширует результаты вызовов fn. Параметр maxSize ограничивает размер кэша: при достижении лимита удаляется самая старая запись (LRU). Функция должна корректно работать с любыми сериализуемыми аргументами.

    Подсказка

    Используй new Map() для кэша. Ключ: JSON.stringify(args). Для LRU: cache.keys().next().value даёт первый (старейший) ключ Map. Проверяй cache.size >= maxSize перед записью.

    Загружаем среду выполнения...
    Загружаем AI-помощника...