Перейти к содержанию

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

Обзор

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

Для начала давайте обсудим два основных аспекта веб-производительности:

  • Производительность загрузки страниц: как быстро приложение отображает содержимое и становится интерактивным при первом посещении. Обычно это измеряется с помощью таких жизненно важных веб-метрик, как Largest Contentful Paint (LCP) («Наибольшая отрисовка контента») и Interaction to Next Paint («Взаимодействие до следующей отрисовки»).

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

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

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

  • Джейсон Миллер рассказывает о типах веб-приложений и их идеальной реализации/доставке в статье Application Holotypes.

Параметры профилирования

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

Для профилирования производительности нагрузки в развёртываниях на продакшене:

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

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

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

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

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

Если ваше основное приложение должно быть SPA, но в нем есть маркетинговые страницы (посадочные, о, блог), отправьте их отдельно! В идеале ваши маркетинговые страницы должны быть развернуты в виде статического HTML с минимальным количеством JS с помощью SSG.

Размер пакета и Tree-shaking

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

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

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

    • При использовании этапа сборки шаблоны предварительно компилируются, поэтому нам не нужно отправлять компилятор Vue в браузер. Это экономит 14 КБ в минимизированной gz-версии JavaScript и позволяет избежать затрат на компиляцию во время выполнения.

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

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

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

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

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

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

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

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

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

js
import { defineAsyncComponent } from 'vue'

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

В приложениях, использующих Vue Router, настоятельно рекомендуется использовать ленивую загрузку для компонентов маршрута. Vue Router имеет явную поддержку ленивой загрузки, отдельную от defineAsyncComponent. Дополнительные сведения см. в разделе Ленивая загрузка маршрутов.

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

Стабильность пропсов

В 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.

Стабильность вычисляемых свойств

В Vue 3.4 и выше, вычисляемое свойство будет вызывать эффекты только тогда, когда его вычисляемое значение изменится по сравнению с предыдущим. Например, следующее вычисление 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

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

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(). Неглубокие («поверхностные») 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 элементов, в котором каждый компонент элемента содержит множество дочерних компонентов. Удаление одной ненужной компонентной абстракции может привести к сокращению сотен экземпляров компонентов.

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