
Vue 3 не только добавил новый синтаксис (Composition API), но и серьёзно обновил движок реактивности. Теперь под капотом используются прокси-объекты (ES6 Proxy), а при отслеживании и изменении данных происходят события Track и Trigger. Эти детали могут быть неочевидны в простых демо-примерах, но становятся крайне важными, когда вы работаете с большими структурами данных или строите действительно масштабные приложения.
С обновлениями до версии 3.5 улучшения стали ещё более заметными: Vue научился лучше обрабатывать глубоко вложенные структуры данных, оптимизировал производительность реактивности и усилил поддержку асинхронных процессов.
В этой статье разберём:
- Реактивность в глубину: как Vue 3 следит за изменениями, что такое Track/Trigger, как оптимизировать работу с вложенными объектами, и какие инструменты для отладки могут помочь. 
- Сложные сценарии с provide/inject и customRef: когда эти механизмы полезны, как управлять глубокой иерархией компонентов, и как customRef решает задачи с debounce. 
- Suspense и асинхронные данные: что такое - <Suspense>, как работает- async setup(), какие преимущества даёт при работе с динамическими компонентами и загрузкой больших данных, а также как обрабатывать ошибки с помощью Error Boundaries.
Поехали!
Реактивность в глубину: Proxy, Track, Trigger
Как Vue 3 следит за изменениями
В Vue 2 механизм реактивности строился на Object.defineProperty, который перехватывал геттеры/сеттеры каждого свойства. Этот подход имел ограничения, например, не отслеживал динамически добавленные свойства и был менее гибким при работе со вложенными структурами.
В Vue 3 вместо этого используется Proxy. Когда вы создаёте реактивные данные (через reactive() или ref()), Vue оборачивает исходный объект в прокси, чтобы ловить все операции чтения/записи:
- track срабатывает, когда мы читаем свойство (геттер). 
- trigger срабатывает, когда мы записываем (сеттер). 
С обновлениями версии 3.5 Vue улучшил работу track и trigger для сложных структур, оптимизировав их производительность. Теперь изменения в глубоко вложенных объектах инициализируются только при необходимости, что снижает накладные расходы.  
Простой пример с effect() (в реальном приложении Vue сама под капотом использует рендер-эффекты):
import { reactive, effect } from 'vue'
const state = reactive({ count: 0 })
effect(() => {
  console.log(`Count is: ${state.count}`)
})
// При изменении state.count (trigger) автоматически вызывается effect
state.count++
// console.log выведет: "Count is: 1"
В реальном приложении вместо effect() Vue под капотом использует собственные эффекты рендера, чтобы обновлять шаблон или virtual DOM.
Track/Trigger на практике
Под капотом всё выглядит так (упрощённо):
- track(target, type, key)- Подпишись на изменения свойства- keyу объекта- target, если при рендере мы читали это свойство.
- trigger(target, type, key, newValue, oldValue)- Уведоми всех подписчиков, когда свойство- keyу объекта- targetменяется.
Если у вас есть глубокие вложенные структуры (например, state.user.profile.address), Vue 3 создаст прокси для каждого уровня, чтобы при доступе к address.city происходил track, а при изменении city - trigger.
Оптимизация при работе с большими объектами
Для работы с большими структурами данных в Vue 3.5 появились дополнительные оптимизации, которые стоит учитывать:
- Ленивая инициализация: Прокси для вложенных объектов создаются только при обращении к этим объектам. Это уменьшает нагрузку на память и улучшает производительность. 
- Использовать - shallowReactiveи- shallowRef.Эти функции поверхностно отслеживают только верхний уровень объекта и не спускаются глубже. Если вам нужно реактивно заменить весь объект целиком, но не важно, что происходит внутри, это может быть отличным решением.
- Делить объект на логические модули. Вместо одного огромного - storeразбивайте данные на более мелкие подхранилища. В экосистеме Vue 3 для этого отлично подойдёт Pinia или несколько отдельных composable-функций.
Инструменты для отладки: Vue Devtools 6+
Когда вы работаете со сложной реактивностью, полезно следить, как Vue отслеживает изменения. В версии Vue Devtools 6 (и выше) есть расширенный вклад Timeline, где можно увидеть события component render, update, и другие - это упрощает понимание, какой именно фрагмент кода или объект триггерит повторный рендер. Также существуют экспериментальные плагины, показывающие track/trigger в реальном времени, но они могут меняться от релиза к релизу.
Сложные сценарии с provide/inject и customRef
provide/inject для больших деревьев
provide и inject позволяют протягивать данные через несколько уровней компонентов без явной передачи пропсов. Это особенно актуально, когда у вас глобальные данные или сервисы (например, тема приложения, текущий пользователь, WebSocket-соединение).
Упрощённый пример:
<!-- App.vue -->
<template>
  <div>
    <ThemeProvider>
      <ChildComponent />
    </ThemeProvider>
  </div>
</template>
<script setup>
// Никакой логики здесь - просто контейнер
</script><!-- ThemeProvider.vue -->
<template>
  <div>
    <slot />
  </div>
</template>
<script setup>
import { provide, reactive } from 'vue'
const themeState = reactive({ 
  color: 'blue',
  fontSize: '16px'
})
// Предоставляем (provide) themeState всем дочерним компонента
provide('themeState', themeState)
</script><!-- ChildComponent.vue -->
<template>
  <div :style="{ color: theme.color, fontSize: theme.fontSize }">
    Я потомок, но мне прилетела тема из ThemeProvider!
  </div>
</template>
<script setup>
import { inject } from 'vue'
const theme = inject('themeState')
</script>Здесь нет необходимости пробрасывать theme через каждый уровень компонентов в виде пропсов. Однако, когда в коде много таких глобальных переменных (цвета, настройки, текущий пользователь и т.д.), будьте аккуратны с выбором ключей (provide('themeState', ...) и т.п.). Если вы используете TypeScript, применяйте символы (Symbol) вместо строк, чтобы избежать коллизий. Suspense и асинхронные данные
Проблемы производительности
Если у вас сложная логика, слушающая изменения в themeState (или другом глобальном объекте), и это приводит к каскадному ререндеру многих компонентов, подумайте о разделении:
- Использовать несколько - provideдля разных сущностей (цвет, шрифт, лейаут).
- Применять - shallowReactive, если нужно менять объект целиком, а не отдельные поля.
- Организовывать обновление только там, где оно действительно нужно, а в остальных местах (детях) использовать мемоизацию (в Composition API это может быть - computed()или закешированные значения).
customRef для тонкой настройки рендера
Иногда нужно полностью контролировать момент, когда произойдёт перерисовка. customRef даёт возможность вручную описать логику при чтении (get) и записи (set). Типичный пример - debounce.
import { customRef } from 'vue'
function useDebouncedRef(value, delay = 500) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track() // подпишемся на чтение
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger() // уведомим, что значение изменилось
        }, delay)
      }
    }
  })
}
export default useDebouncedRef
Как использовать:
<template>
  <input v-model="search" placeholder="Поиск..." />
  <p>Вы ввели: {{ search }}</p>
</template>
<script setup>
import { ref } from 'vue'
import useDebouncedRef from './useDebouncedRef.js'
const search = useDebouncedRef('', 500)
</script>Теперь при вводе в <input>, ререндер срабатывает не на каждый символ, а только через 500 мс тишины. Это заметно улучшает производительность, если, скажем, мы вызываем тяжелый запрос по мере ввода.
Suspense и асинхронные данные
Что такое ?
<Suspense> - это специальный компонент в Vue 3, который позволяет показывать заглушку (fallback), пока внутри него происходит асинхронная загрузка данных. Идея пришла из React (React Suspense), но Vue адаптировал её под свою модель.  
Пример:
<template>
  <Suspense>
    <template #default>
      <AsyncDataComponent />
    </template>
    <template #fallback>
      Загрузка...
    </template>
  </Suspense>
</template>
<script setup>
import AsyncDataComponent from './AsyncDataComponent.vue'
</script> Пока AsyncDataComponent не загрузит данные (например, делает запрос на сервер в setup()), <Suspense> будет отображать блок <template #fallback>. Как только данные будут получены, рендерится нормальный контент.  
async setup() в компонентах
 С приходом Composition API мы можем сделать setup() асинхронным. Упрощённый пример: 
<template>
  <div>
    <h1>Пользователь: {{ user.name }}</h1>
  </div>
</template>
<script setup>
import { ref } from 'vue'
// Эмулируем запрос
async function fetchUser(id) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
  return await res.json()
}
const user = ref(null)
const props = defineProps({
  userId: {
    type: Number,
    required: true
  }
})
onBeforeMount(async () => {
  user.value = await fetchUser(props.userId)
})
</script>
Но чтобы <Suspense> понимал, что внутри происходит асинхронщина, Vue проверяет, есть ли в setup() Promise (или onBeforeMount(async () => ...) возвращает Promise). Если да, <Suspense> будет ждать, пока этот Promise не завершится.
Prefetching и graceful fallback
- Prefetching. Если вы знаете, что компонент с асинхронной логикой скоро понадобится (пользователь, например, наводит курсор на ссылку), можно заранее инициировать запрос. К моменту рендера - <Suspense>будет ждать намного меньше, а пользователь увидит готовые данные практически моментально.
- 
Graceful fallback и Error Boundaries. Если при загрузке данных произошла ошибка, можно показать не только "Загрузка…", но и "Ошибка загрузки данных" или другой альтернативный контент. Для этого во Vue 3 можно использовать Error Boundaries, чтобы перехватывать ошибки в асинхронных компонентах и показывать соответствующий интерфейс. Примерно это может выглядеть так: <template> <ErrorBoundary> <Suspense> <template #default> <AsyncDataComponent /> </template> <template #fallback> Загрузка... </template> </Suspense> </ErrorBoundary> </template>Где ErrorBoundary- компонент, который внутри использует Vue-хукиerrorCaptured,onErrorCapturedили специальную логику для перехвата и отображения ошибок.
Заключение
Итоги
Мы рассмотрели:
- Реактивность во Vue 3: как прокси-объекты помогают трекать изменения, что такое Track/Trigger и почему это эффективно. 
- provide/inject и - customRef: удобный способ передавать данные между компонентами без пропсов, и тонкая настройка рендера с помощью- customRef.
- <Suspense>: как он упрощает работу с асинхронными компонентами и даёт пользователям дружелюбный loading вместо мгновенного белого экрана.
Когда это применять?
- Глубокую реактивность - в большинстве проектов по умолчанию. Но следите за производительностью, избегайте реактивного монстра. 
- provide/inject- если у вас сложная архитектура, где пропсы становятся громоздкими. Это особенно актуально для глобальных зависимостей (тема, текущий пользователь, глобальные сервисы).
- customRef- в точечных случаях, когда нужно особое поведение при записи значения (debounce, throttle, сериализация и т.д.).
- <Suspense>- при работе с асинхронным кодом, чтобы дать пользователю плавный опыт загрузки, особенно если данные тяжёлые.
Надеюсь, этот материал поможет глубже понять механику Vue 3. Экспериментируйте с реактивностью, используйте провайдеры и асинхронные компоненты для оптимизации рабочих процессов - и ваш код станет чище, а приложения - быстрее!
Если вы хотите глубже погрузиться в тему оптимизации JavaScript и TypeScript, советую также ознакомиться с моими другими статьями на Habr, где я делюсь опытом написания продуктивного кода и улучшения пользовательского опыта :)
Шина между Веб-воркерами и основным потоком. Ускоряем работу JavaScript
В этой статье я рассказываю о созданном npm-пакете web-worker-bus, который упрощает взаимодействие веб-воркеров с основным потоком. Пакет помогает повысить производительность и разгрузить UI-поток, особенно актуально для крупных и ресурсоёмких приложений — будь то на Vue, React или любом другом фреймворке.
Мощь декораторов TypeScript на живых примерах
Здесь я показываю, как декораторы в TypeScript упрощают код, устраняя дублирование и улучшая читаемость. Рассматривается несколько реальных примеров, где сквозная функциональность (логирование, кеширование, валидация и т.д.) оформлена при помощи декораторов без изменения основной бизнес-логики приложения.
 
          