Vue
Производительность Vue
Таблица заказов на 5000 строк подгружает данные за 80 миллисекунд, но при наборе текста в фильтре каждый символ подвешивает интерфейс на четверть секунды. Профайлер Vue DevTools показывает, что один keypress перерисовывает все 5000 строк, хотя на экране помещается 30. Дело не в данных и не в сети: реактивность отслеживает глубокий объект целиком, шаблон зовёт метод-форматтер на каждой строке заново, а в DOM живёт пять тысяч узлов разом. Этот урок про инструменты, которые убирают лишнюю работу: shallowRef и markRaw, v-once и v-memo, виртуальный скролл и computed.
- Vue DevTools (вкладка Performance): профилирует длительность и причину каждого компонентного ре-рендера
- Линии Inertia и админки на Vue: тяжёлые таблицы на десятки тысяч строк держатся на виртуальном скролле
- Графические редакторы и карты: экземпляры сторонних библиотек оборачивают в markRaw, чтобы не делать их реактивными
- Дашборды реального времени: shallowRef для больших снапшотов данных, которые меняются целиком, а не по полю
- Чаты и ленты: v-memo на элементах списка пропускает перерисовку строк, чьи данные не изменились
Предварительные знания
- Понимание ref и reactive и того, что глубокая реактивность отслеживает вложенные свойства
- Знакомство с shallowRef и markRaw на уровне идеи
- Базовая работа с шаблонами, директивами и computed-свойствами
Дисциплина shallowRef и markRaw
По умолчанию reactive и ref делают реактивность глубокой: Vue рекурсивно оборачивает в прокси каждое вложенное свойство, чтобы отследить любое изменение. Для небольшого объекта это незаметно. Но если в состоянии лежит снапшот из десяти тысяч элементов или экземпляр графической библиотеки с сотней внутренних полей, рекурсивное оборачивание само по себе стоит времени и памяти, а отслеживание изменений, которые никого не интересуют, добавляет накладные расходы на каждое обновление.
shallowRef отслеживает только присваивание самого .value. Вложенные поля не оборачиваются в прокси, поэтому большой массив или объект не платит за глубокую реактивность. Это идеальный выбор для данных, которые приходят и заменяются целиком: ответ API, кадр данных, снапшот состояния. Когда нужно мутировать вложенное поле и всё же уведомить шаблон, используется triggerRef.
markRaw помечает объект флагом, который запрещает Vue делать его реактивным. Это спасает от двух бед: лишнего оборачивания тяжёлого экземпляра и поломки самой библиотеки, которая может хранить внутри ссылки на DOM-узлы или приватные поля, не переживающие проксирование. Экземпляры карт, чартов, редакторов и WebGL-сцен почти всегда оборачивают в markRaw.
В состоянии лежит экземпляр библиотеки карт Leaflet с сотнями внутренних свойств и ссылками на DOM. Как его хранить?
Пропуск перерисовки: v-once, v-memo, computed
Самая дешёвая работа это та, которую не делают. Vue даёт три инструмента, чтобы пропустить ненужную перерисовку. v-once рендерит фрагмент ровно один раз и навсегда замораживает его. v-memo пропускает обновление поддерева, пока значения в его массиве зависимостей не изменились. computed кеширует результат и пересчитывает его только при изменении зависимостей, в отличие от метода, который выполняется на каждый рендер.
v-memo принимает массив значений. Если все они совпали с предыдущим рендером, Vue полностью пропускает обновление этого элемента и его детей. На большом списке это убирает основную стоимость: при выделении одной строки перерисовывается только она и предыдущая выделенная, а не весь список. Важно перечислить в массиве все значения, от которых реально зависит разметка, иначе обновление пропустится, когда оно было нужно.
v-memo легко применить неправильно. Если в массиве зависимостей забыть значение, от которого зависит шаблон, элемент перестанет обновляться при его изменении и появится визуальный баг, который трудно отследить. v-memo стоит вводить только на доказанно горячих списках и всегда перечислять полный набор зависимостей.
Почему для итоговой суммы в шаблоне предпочтительнее computed, а не метод?
Большие списки и измерение
Даже идеально оптимизированный компонент строки не спасёт, если в DOM одновременно живут десять тысяч узлов. Браузер тратит время на layout и paint каждого, а память растёт линейно. Виртуальный скролл решает это, держа в DOM только видимые элементы плюс небольшой буфер. При прокрутке узлы переиспользуются: то, что ушло за край экрана, перерисовывается под новые данные снизу. Пользователь видит полный список, но в DOM лежат три десятка строк, а не десять тысяч.
Любую оптимизацию начинают не с догадки, а с измерения. Вкладка Performance в Vue DevTools записывает сессию взаимодействия и показывает каждый компонентный ре-рендер: его длительность, частоту и причину. Если один keypress перерисовывает поддерево из тысяч компонентов, это видно сразу, и понятно, куда ставить v-memo или виртуальный скролл. Без профайлера оптимизация превращается в стрельбу вслепую, когда время тратится на компоненты, которые и так дёшевы.
| Симптом | Вероятная причина | Инструмент |
|---|---|---|
| Один клик перерисовывает весь список | Нет мемоизации строк | v-memo по ключевым полям |
| Скролл по 20k строкам тормозит | Все узлы в DOM одновременно | Виртуальный скролл |
| Ввод в фильтр подвешивает UI | Метод-форматтер на каждой строке | computed вместо метода |
| Загрузка большого ответа лагает | Глубокое проксирование снапшота | shallowRef |
Сначала измеряют, потом оптимизируют. Преждевременная оптимизация добавляет v-memo и markRaw туда, где компонент и так перерисовывается за доли миллисекунды, усложняя код без выигрыша. Вкладка Performance показывает реальные горячие точки, и работа идёт только по ним.
Список из 20000 строк тормозит при скролле, хотя каждая строка мемоизирована через v-memo. Что устранит главную причину?
Связь с другими темами
Урок опирается на реактивность и продолжает тему рендеринга:
- Глубокая реактивность — Лишние ре-рендеры начинаются с того, что реактивность отследила слишком много зависимостей
- shallowRef и markRaw — Поверхностная реактивность и исключение объектов это первый рычаг производительности
- Гидрация и гибридный рендеринг — Производительность после гидрации определяется стоимостью обновлений на клиенте
Итог
- shallowRef делает реактивным только присваивание самого значения, а не вложенные поля, что выгодно для крупных снапшотов, меняющихся целиком
- markRaw помечает объект как нереактивный навсегда, что нужно для экземпляров сторонних библиотек, которые незачем оборачивать в прокси
- v-once рендерит фрагмент один раз и больше никогда не обновляет его, а v-memo пропускает обновление поддерева, пока массив зависимостей не изменился
- Виртуальный скролл держит в DOM только видимые элементы вместо тысяч узлов, что снимает основную стоимость больших списков
- computed кешируется по зависимостям, а метод в шаблоне выполняется на каждый рендер, поэтому для вычисляемых значений выбирается computed
- Вкладка Performance в Vue DevTools показывает, какой компонент перерисовывается, как часто и почему, и это основа любой оптимизации
Связанные уроки
- vue-37-hydration-rendering — Гидрация задаёт стартовую цену интерактивности, а этот урок снижает цену каждого последующего обновления
- vue-18-reactivity-deep — Оптимизация ре-рендеров опирается на понимание того, как реактивность отслеживает зависимости