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 остаётся актуальной для тяжёлых данных