javascript

trackOpBits во Vue 3: как битовые маски ускоряют ReactiveEffect

  • суббота, 14 февраля 2026 г. в 00:00:07
https://habr.com/ru/articles/996052/

Привет, Хабр.

Это моя первая статья здесь. Долгое время не решался что-то публиковать, хотя регулярно читал и разбирал материалы других авторов.

Для первой публикации я выбрал тему внутренней оптимизации реактивности во Vue 3 — trackOpBits и работу ReactiveEffect. Этот механизм почти не заметен при обычной работе с фреймворком, но он напрямую влияет на производительность рендера компонентов и поведение вложенных computed.

В статье разберём, какую проблему решает trackOpBits, как именно он используется внутри системы реактивности и почему эта оптимизация важна в реальных приложениях.

Кратко о ReactiveEffect

В Vue 3 любая реактивная логика завязана на ReactiveEffect:

  • рендер компонента

  • computed

  • watchEffect

  • watch

Все они внутри — effect’ы.

Упрощённо:

class ReactiveEffect {
  fn
  deps = []
  active = true
  trackOpBits = 0
}

Во время выполнения fn effect становится активным, и все обращения к реактивным данным регистрируются как зависимости.

Где возникает реальная проблема

Посмотрим на реальный сценарий, который происходит в каждом Vue-приложении.

Пример: компонент + computed + computed

const state = reactive({
  price: 100,
  count: 2,
  tax: 0.2
})

const total = computed(() => {
  return state.price * state.count
})

const totalWithTax = computed(() => {
  return total.value * (1 + state.tax)
})

А теперь представим компонент:

const Comp = {
  setup() {
    return () => {
      return h('div', totalWithTax.value)
    }
  }
}

Что реально происходит при первом рендере:

  1. Создаётся render effect компонента

  2. Внутри рендера читается totalWithTax.value

  3. totalWithTax — это computed, у него свой effect

  4. Внутри totalWithTax читается total.value

  5. total — ещё один computed, ещё один effect

  6. Внутри total читаются: state.price, state.count

Итого, мы имеем вложенность effect’ов глубиной 3:

render effect
 └─ computed(totalWithTax)
     └─ computed(total)
         └─ reactive state

Что пойдёт не так без оптимизаций

Наивная реализация реактивности делала бы следующее:

  • каждый get: добавляет activeEffect в dep

  • каждый effect: при новом запуске очищает все deps и пересобирает их заново

При вложенных effect’ах это означает:

  • повторные добавления одного и того же effect’а

  • лишние проверки

  • постоянные cleanup даже там, где зависимости не менялись

На больших деревьях компонентов и сложных computed это быстро становится дорогой операцией.

effectTrackDepth — контроль глубины

Во Vue 3 есть глобальный счётчик:

let effectTrackDepth = 0

Каждый раз, когда начинается выполнение effect’а:

effectTrackDepth++

А при завершении — уменьшается.

Это позволяет Vue понимать, на каком уровне вложенности сейчас идёт сбор зависимостей.

Что такое trackOpBits

trackOpBits — это битовая маска, хранящая информацию о том,
на каких уровнях глубины effect уже был зарегистрирован в зависимостях.

Для текущей глубины вычисляется бит:

const trackOpBit = 1 << effectTrackDepth

Этот бит используется как флаг.

Как это работает на практике

Когда выполняется track(dep):

  1. Vue проверяет: есть ли у effect’а trackOpBit для текущей глубины

  2. Если бит уже установлен: effect не добавляется повторно в dep

  3. Если бита нет: effect добавляется и выставляется бит

if (!(effect.trackOpBits & trackOpBit)) {
  dep.add(effect)
  effect.trackOpBits |= trackOpBit
}

Таким образом:

  • один и тот же effect не может быть добавлен дважды

  • Vue избегает лишних операций при вложенных вычислениях

Почему это особенно важно для computed

computed во Vue 3:

  • ленивые

  • кешируемые

  • могут вызываться из других computed и из рендера

Без trackOpBits каждый доступ к .value во вложенных цепочках приводил бы к:

  • повторному трекингу

  • очистке зависимостей

  • лишним аллокациям

С битовой маской:

  • зависимости собираются один раз на уровень

  • повторные чтения становятся почти бесплатными

Ограничение по глубине

Во Vue 3 есть ограничение на максимальную глубину, где используется битовая оптимизация
(на момент написания — 30 уровней).

После этого Vue аккуратно откатывается к более простой логике трекинга, без битов. Это сделано, чтобы:

  • избежать переполнения битовой маски

  • сохранить предсказуемое поведение

На практике в обычных приложениях до этого лимита почти никогда не доходят.

Почему этого не было во Vue 2

Во Vue 2:

  • реактивность строилась на Object.defineProperty

  • не было ReactiveEffect в текущем виде

  • не было чёткого контроля вложенности эффектов

Архитектура Vue 3 (Proxy + эффекты) позволила:

  • отслеживать глубину

  • использовать битовые маски

  • минимизировать работу GC и аллокации

trackOpBits — пример оптимизации, которая стала возможной только после полной переработки реактивности.

Нужно ли это знать обычному разработчику

Скорее нет — Vue отлично работает и без этого знания.

Но если вы:

  • дебажите странные перерендеры

  • пишете сложные computed

  • работаете с производительностью

  • или просто хотите понимать, что происходит под капотом

— знание таких деталей сильно упрощает мышление о поведении фреймворка.

Заключение

trackOpBits — маленькая, но очень важная часть реактивности Vue 3.
Она позволяет:

  • эффективно работать с вложенными effect’ами

  • избежать лишнего трекинга

  • сделать computed и рендер компонентов действительно быстрыми

Именно такие низкоуровневые решения создают ощущение, что Vue 3 «просто летает», даже в больших приложениях.

Если тема будет интересна — можно отдельно разобрать:

  • scheduler эффектов

  • очереди pre / post flush

  • или жизненный цикл рендер effect’а компонента

Спасибо за внимание.