Skip to content

Производительность

Обзор

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

Прежде всего, давайте обсудим два основных аспекта веб-производительности:

  • Производительность загрузки страниц: скорость отображения содержимого и интерактивности приложения при первом посещении. Обычно это измеряется с помощью таких жизненно важных метрик, как Largest Contentful Paint (LCP) и First Input Delay (FID).

  • Производительность обновления: скорость обновления приложения в ответ на ввод пользователя. Например, скорость обновления списка при вводе пользователем текста в поисковую строку или скорость переключения страницы при нажатии пользователем на навигационную ссылку в одностраничном приложении (SPA).

Идеально было бы максимизировать оба показателя, однако различные фронтенд-архитектуры, как правило, влияют на то, насколько легко достичь желаемой производительности в этих аспектах. Кроме того, тип создаваемого приложения в значительной степени влияет на то, чему следует отдать предпочтение в плане производительности. Поэтому первым шагом к обеспечению оптимальной производительности является выбор архитектуры, соответствующей типу создаваемого приложения:

  • Обратитесь к разделу Способы использования Vue, чтобы узнать, как можно использовать Vue различными способами.

  • В статье Application Holotypes Джейсон Миллер рассматривает типы веб-приложений и соответствующие им идеальные варианты реализации/развёртывания.

Варианты оценки производительности

Чтобы повысить эффективность работы, необходимо знать, как ее измерить. Существует ряд замечательных инструментов, которые могут помочь в этом:

Для оценки производительности загрузки production-сборки:

Для оценки производительности при локальной разработке:

Оптимизация загрузки страниц

Существует множество аспектов оптимизации производительности загрузки страниц, не зависящих от фреймворка. Ознакомьтесь с этим руководством web.dev, чтобы получить исчерпывающую информацию. Здесь же мы сосредоточимся на методах, характерных для Vue.

Выбор правильной архитектуры

Если ваш сценарий использования чувствителен к производительности загрузки страницы, избегайте использования его в качестве чисто клиентского SPA. Нужно, чтобы ваш сервер напрямую передавал HTML с содержимым страницы, которое хотят увидеть пользователи. Чисто клиентский рендеринг страдает от медленной скорости отображения содержимого. Этого можно избежать с помощью отрисовки на стороне сервера (SSR) или статической генерации сайта (SSG). Ознакомьтесь с руководством SSR Guide, чтобы узнать о выполнении SSR во Vue. Если ваше приложение не предъявляет высоких требований к интерактивности, вы также можете использовать традиционный бэкенд-сервер для рендеринга HTML и расширить его с помощью Vue на клиенте. Если ваше основное приложение должно быть SPA, но в нём есть маркетинговые страницы (посадочные, о компании, блог), публикуйте их отдельно! В идеале маркетинговые страницы должны быть развёрнуты как статический HTML с минимальным количеством JS с помощью SSG.

Размер сборки и tree-shaking

Одним из наиболее эффективных способов повышения производительности загрузки страниц является развертывание более компактных бандлов JavaScript. Вот несколько способов уменьшить размер бандла при использовании Vue:

  • По возможности используйте шаг сборки.

    • Многие API Vue являются "tree-shakable", если они собираются с помощью современных средств сборки. Например, если вы не используете встроенный компонент <Transition>, он не будет включен в конечный бандл. Tree-shaking может также удалить другие неиспользуемые модули в исходном коде.

    • При использовании шага сборки шаблоны предварительно компилируются, поэтому нам не нужно поставлять компилятор Vue в браузер. Это позволяет сэкономить 14kb min+gzipped JavaScript и избежать затрат на компиляцию во время выполнения.

  • Будьте внимательны с размером бандла при введении новых зависимостей! В реальных приложениях раздутые бандлы чаще всего являются результатом внедрения тяжелых зависимостей без осознания этого.

    • При использовании шага сборки отдавайте предпочтение зависимостям, предлагающим форматы ES-модулей и поддерживающих tree-shaking. Например, выберите lodash-es вместо lodash.

    • Проверьте размер зависимости и оцените, стоит ли она той функциональности, которую предоставляет. Обратите внимание, что если зависимость поддерживает tree-shaking, то фактическое увеличение размера будет зависеть от API, которые вы фактически импортируете из нее. Для быстрой проверки можно использовать такие инструменты, как bundlejs.com, но наиболее точным всегда будет измерение с помощью реальной настройки сборки.

  • Если вы используете Vue в основном для прогрессивного улучшения и предпочитаете избежать шага сборки, то вместо него используйте petite-vue (всего 6kb).

Разделение кода

Разделение кода — это разделение бандла приложения на несколько небольших фрагментов, которые затем могут загружаться по требованию или параллельно. При правильном разделении кода, функции, необходимые при загрузке страницы, могут быть загружены сразу, а дополнительные фрагменты будут загружаться отложено только при необходимости, что повышает производительность.

Такие бандлеры, как Rollup (на котором основан Vite) или webpack, могут автоматически создавать разделенные фрагменты, распознавая синтаксис динамического импорта ESM:

js
// lazy.js и его зависимости будут выделены в отдельный фрагмент
// и загружаться только при вызове `loadLazy()`.
function loadLazy() {
  return import('./lazy.js')
}

Ленивую загрузку лучше всего использовать для функций, которые не нужно выполнять сразу после начальной загрузки страницы. В приложениях Vue это можно использовать в сочетании с асинхронными компонентами для создания разделенных фрагментов для деревьев компонентов:

js
import { defineAsyncComponent } from 'vue'

// для Foo.vue и его зависимостей создается отдельный чанк.
// он извлекается только по требованию, когда асинхронный компонент
// рендерится на странице.
const Foo = defineAsyncComponent(() => import('./Foo.vue'))

В приложениях, использующих Vue Router, настоятельно рекомендуется использовать ленивую загрузку компонентов маршрута. Vue Router имеет явную поддержку ленивой загрузки, отдельную от defineAsyncComponent. Более подробная информация приведена в разделе Lazy Loading Routes.

Оптимизации обновления

Стабильность входных параметров

Во Vue дочерний компонент обновляется только тогда, когда хотя бы один из полученных им входных параметров изменился. Рассмотрим следующий пример:

template
<ListItem
  v-for="item in list"
  :id="item.id"
  :active-id="activeId" />

Внутри компонента <ListItem> он использует свои входные параметры id и activeId для определения того, является ли он активным элементом в данный момент. Это работает, но проблема заключается в том, что при изменении activeId необходимо обновлять каждый <ListItem> в списке!

В идеале, обновляться должны только те элементы, активный статус которых изменился. Этого можно добиться, если перенести вычисление активного состояния в родительский элемент, а вместо этого заставить <ListItem> непосредственно принимать свойство active:

template
<ListItem
  v-for="item in list"
  :id="item.id"
  :active="item.id === activeId" />

Теперь для большинства компонентов входной параметр active будет оставаться неизменным при изменении activeId, поэтому обновлять его больше не нужно. В целом, идея состоит в том, чтобы сохранить входные параметры, передаваемые дочерним компонентам, как можно более стабильными.

v-once

v-once - это встроенная директива, которая может быть использована для вывода содержимого, зависящего от данных во время выполнения программы, но не требующего обновления. При этом всё поддерево, для которого она используется, будет пропускаться при всех последующих обновлениях. Более подробная информация приведена в справочнике API.

v-memo

v-memo - это встроенная директива, которая может быть использована для условного пропуска обновления больших поддеревьев или списков v-for. Более подробная информация приведена в справочнике API.

Стабильность у computed свойств

Начиная с 3.4, computed свойство будет вызывать эффект только в том случае, если его вычисленное значение изменилось по сравнению с предыдущим. Например, computed свойство isEven вызывает эффект, только если возвращаемое значение изменилось с true на false, или наоборот:

js
const count = ref(0)
const isEven = computed(() => count.value % 2 === 0)

watchEffect(() => console.log(isEven.value)) // true

// Это не будет запускать вывод логов, так как вычисленное значение остается `true`
count.value = 2
count.value = 4

Это уменьшает количество ненужных срабатываний эффектов, но, к сожалению, не работает, если computed создает новый объект при каждом вычислении:

js
const computedObj = computed(() => {
  return {
    isEven: count.value % 2 === 0
  }
})

Поскольку каждый раз создается новый объект, новое значение технически всегда отличается от старого. Даже если свойство isEven остается неизменным, Vue не сможет узнать об этом, пока не выполнит глубокое сравнение старого и нового значения. Такое сравнение может быть дорогостоящим и, скорее всего, не стоит того.

Вместо этого мы можем оптимизировать этот процесс, вручную сравнивая новое значение со старым и при необходимости возвращая старое значение, если мы знаем, что ничего не изменилось:

js
const computedObj = computed((oldValue) => {
  const newValue = {
    isEven: count.value % 2 === 0
  }
  if (oldValue && oldValue.isEven === newValue.isEven) {
    return oldValue
  }
  return newValue
})

Пример в песочнице

Обратите внимание, что перед сравнением и возвратом старого значения вы всегда должны выполнить вычисление полностью, чтобы при каждом запуске можно было собрать одни и те же зависимости.

Общие оптимизации

Следующие советы влияют как на загрузку страницы, так и на производительность обновления.

Виртуализация больших списков

Одной из наиболее распространённых проблем производительности во всех фронтенд-приложениях является рендеринг больших списков. Каким бы производительным не был фреймворк, рендеринг списка с тысячами элементов будет медленным из-за огромного количества узлов DOM, которые необходимо обработать браузеру.

Однако нам не обязательно отображать все эти узлы сразу. В большинстве случаев размер экрана пользователя позволяет отобразить лишь небольшое подмножество нашего большого списка. Мы можем значительно повысить производительность с помощью виртуализации списка — техники отображения в большом списке только тех элементов, которые в данный момент находятся в области просмотра или рядом с ней.

Реализовать виртуализацию списков не так просто, к счастью, существуют библиотеки сообщества, которые можно непосредственно использовать:

Уменьшение накладных расходов на реактивность для больших неизменяемых структур

Система реактивности Vue по умолчанию является глубокой. Хотя это делает управление состоянием интуитивно понятным, при больших объёмах данных это создаёт определенный уровень накладных расходов, поскольку при каждом обращении к свойствам запускаются прокси-ловушки, выполняющие отслеживание зависимостей. Обычно это становится заметным при работе с большими массивами глубоко вложенных объектов, когда при одном рендере необходимо получить доступ к 100 000+ свойствам, поэтому это должно влиять только на очень специфические случаи использования.

Vue предоставляет возможность отказаться от глубокой реактивности, используя shallowRef() и shallowReactive(). Shallow API позволяет создать состояние, которое является реактивным только на корневом уровне, а все вложенные объекты остаются нетронутыми. Это обеспечивает быстрый доступ к вложенным свойствам, но в качестве компромисса мы должны рассматривать все вложенные объекты как неизменяемые, а обновления могут быть вызваны только заменой корневого состояния:

js
const shallowArray = shallowRef([
  /* большой список глубинных объектов */
])

// это не вызовет обновления...
shallowArray.value.push(newObject)
// это вызовет:
shallowArray.value = [...shallowArray.value, newObject]

// это не вызовет обновления...
shallowArray.value[0].foo = 1
// это вызовет:
shallowArray.value = [
  {
    ...shallowArray.value[0],
    foo: 1
  },
  ...shallowArray.value.slice(1)
]

Избегайте ненужных компонентных абстракций

Иногда для улучшения абстракции или организации кода мы можем создавать компоненты без рендеринга или компоненты более высокого порядка (т.е. компоненты, которые рендерят другие компоненты с дополнительными входными параметрами). Хотя в этом нет ничего плохого, следует помнить, что экземпляры компонентов гораздо дороже обычных узлов DOM, и создание слишком большого их количества в результате использования шаблонов абстракции приведет к снижению производительности.

Заметим, что удаление всего нескольких экземпляров не даст заметного эффекта, поэтому не стоит беспокоиться, если компонент будет отображаться в приложении всего несколько раз. Лучший сценарий для рассмотрения этой оптимизации — опять же большие списки. Представьте себе список из 100 элементов, в котором каждый компонент элемента содержит множество дочерних компонентов. Удаление одной ненужной абстракции компонента здесь может привести к сокращению сотен экземпляров компонентов.

ПроизводительностьУже загружено