Vue

shallowRef, markRaw и дисциплина реактивности

Дашборд аналитики держит в памяти таблицу на 50 000 строк, пришедшую из API. Разработчик кладёт её в reactive, и интерфейс начинает заметно подтормаживать при первом рендере. Профайлер показывает: Vue рекурсивно обернул каждую из 50 000 строк и каждое поле в Proxy, хотя данные приходят целиком и никогда не правятся по одному полю. Замена reactive на shallowRef убирает лаг полностью - реактивность нужна была только на уровне ссылки, а не каждого вложенного значения.

  • Таблицы и гриды (AG Grid, таблицы в админках): десятки тысяч строк из API, которые заменяются целиком, а не правятся по ячейке
  • Карты и геоданные: экземпляр карты Leaflet или Mapbox оборачивать в Proxy бессмысленно и вредно
  • Графики: instance Chart.js или ECharts хранится в компоненте, но управляется императивно, не через реактивность
  • WebSocket-потоки котировок: большие массивы тиков приходят пачками и заменяются ссылкой целиком
  • Кэш ответов API: неизменяемые снимки данных, где глубокая реактивность только тратит память и CPU

Предварительные знания

  • Понимание, что reactive в Vue 3 строится на JavaScript Proxy
  • Знание, что reactive оборачивает вложенные объекты рекурсивно (глубокая реактивность)
  • Различие между ref (значение в .value) и reactive (объект целиком)

Цена глубокой реактивности

reactive и обычный ref с объектом внутри дают глубокую реактивность: при обращении к любому вложенному свойству Vue создаёт для него отдельный Proxy. Для формы из десяти полей это незаметно. Для массива из 50 000 объектов с вложенными полями создаётся столько же Proxy-обёрток при первом доступе, плюс трекинг зависимостей на каждое поле. Получается то, что в сообществе называют налогом на реактивность: память и время тратятся на отслеживание изменений, которых никогда не будет.

Ключевой вопрос перед выбором: будет ли код менять отдельные вложенные поля так, чтобы интерфейс должен был реагировать? Если данные приходят из API одним куском и заменяются целиком (новый ответ - новая ссылка), глубокая реактивность не нужна совсем. Достаточно реактивности на уровне самой ссылки.

Налог проявляется не на объявлении, а при первом доступе к вложенным свойствам (например, при рендере). Поэтому проблема часто всплывает как лаг первого кадра большого списка, а не как медленная инициализация.

Почему reactive на массиве из 50 000 объектов может вызвать лаг при первом рендере?

shallowRef и shallowReactive

shallowRef делает реактивной только саму ссылку. Чтение и запись .value отслеживаются, но содержимое объекта внутри остаётся сырым: мутация вложенного поля не вызовет обновления. Это идеально для данных, которые меняются заменой целиком.

shallowReactive делает реактивными только корневые свойства объекта. Изменение поля верхнего уровня отслеживается, а мутация вложенного объекта - нет. Применяется реже shallowRef, но полезно для объектов состояния, где реактивны только верхние ключи.

APIЧто реактивноКогда применять
ref / reactiveВсё дерево рекурсивноОбычное состояние UI: формы, фильтры, флаги
shallowRefТолько .value (ссылка)Большие данные, заменяемые целиком; instance внешних библиотек
shallowReactiveТолько корневые свойстваОбъект, где реактивны лишь верхние ключи

При shallowRef обновление по месту делается через triggerRef(rows) после ручной мутации содержимого, если по какой-то причине нельзя заменить ссылку целиком. Но обычно чище заменить .value на новый массив.

Данные приходят из API одним массивом и при обновлении заменяются целиком. Что выбрать?

markRaw и toRaw

markRaw навсегда помечает объект как нереактивный. Даже если такой объект положить внутрь reactive, Vue не станет оборачивать его в Proxy. Это нужно для экземпляров внешних библиотек: карта Mapbox, instance Chart.js, класс из сторонней SDK. Их внутреннее состояние большое и управляется императивно, а оборачивание в Proxy ломает работу библиотеки или просто тратит ресурсы зря.

toRaw решает обратную задачу: возвращает исходный сырой объект из реактивного Proxy. Это полезно, когда нужно передать данные во внешний код, который ломается на Proxy, сравнить объекты по ссылке или избежать накладных расходов на трекинг в горячем цикле.

markRaw и toRaw - инструменты дисциплины, а не оптимизации на каждый день. Реактивность Vue быстра, и для обычного состояния её цена незаметна. Доставать эти утилиты стоит на крупных данных и при интеграции с императивными библиотеками, где Proxy мешает или избыточен.

Экземпляр карты Mapbox хранится в свойстве reactive-объекта и работает некорректно. Что разумно сделать?

Связь с другими темами

Урок продолжает разбор реактивности и готовит к утилитам вокруг ref:

  • Глубокая реактивность — Этот урок показывает оборотную сторону рекурсивного оборачивания: его цену на больших данных
  • Утилиты реактивности — toRaw логически входит в тот же набор, что toRef, toRefs и unref
  • Движок alien-signals — Vue 3.6 ускоряет реактивность в ядре, но решения о shallow принимает по-прежнему разработчик

Итог

  • Глубокая реактивность reactive рекурсивно оборачивает в Proxy каждый вложенный объект, и на больших структурах это заметная цена по памяти и CPU
  • shallowRef делает реактивной только саму ссылку (.value), не трогая внутренности - подходит для данных, которые заменяются целиком
  • shallowReactive делает реактивными только корневые свойства объекта, оставляя вложенные значения сырыми
  • markRaw навсегда помечает объект как нереактивный, и Vue никогда не обернёт его в Proxy даже внутри reactive
  • toRaw возвращает исходный сырой объект из реактивного Proxy - полезно для передачи во внешние библиотеки и сравнения по ссылке

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

  • vue-18-reactivity-deep — Чтобы экономить на реактивности, нужно понимать, как именно reactive рекурсивно оборачивает объект в Proxy
  • vue-21-reactivity-utilities — toRaw из этого урока тесно связан с toRef/toRefs - тот же набор утилит вокруг ref и reactive
  • vue-23-alien-signals — Движок 3.6 ускоряет реактивность в целом, но дисциплина shallow и markRaw остаётся актуальной для тяжёлых данных
shallowRef, markRaw и дисциплина реактивности

0

1

Войти