Производительность
Вакансии vuejobs.com
Обзор
Vue спроектирован таким образом, чтобы обеспечить производительность для большинства распространенных сценариев использования без необходимости ручной оптимизации. Однако всегда есть сложные сценарии, в которых требуется дополнительная тонкая настройка. В этом разделе мы обсудим, на что следует обратить внимание, когда речь идет о производительности приложения Vue.
Прежде всего, давайте обсудим два основных аспекта веб-производительности:
Производительность загрузки страниц: скорость отображения содержимого и интерактивности приложения при первом посещении. Обычно это измеряется с помощью таких жизненно важных метрик, как Largest Contentful Paint (LCP) и First Input Delay (FID).
Производительность обновления: скорость обновления приложения в ответ на ввод пользователя. Например, скорость обновления списка при вводе пользователем текста в поисковую строку или скорость переключения страницы при нажатии пользователем на навигационную ссылку в одностраничном приложении (SPA).
Идеально было бы максимизировать оба показателя, однако различные фронтенд-архитектуры, как правило, влияют на то, насколько легко достичь желаемой производительности в этих аспектах. Кроме того, тип создаваемого приложения в значительной степени влияет на то, чему следует отдать предпочтение в плане производительности. Поэтому первым шагом к обеспечению оптимальной производительности является выбор архитектуры, соответствующей типу создаваемого приложения:
Обратитесь к разделу Способы использования Vue, чтобы узнать, как можно использовать Vue различными способами.
В статье Application Holotypes Джейсон Миллер рассматривает типы веб-приложений и соответствующие им идеальные варианты реализации/развертывания.
Варианты оценки
Чтобы повысить эффективность работы, необходимо знать, как ее измерить. Существует ряд замечательных инструментов, которые могут помочь в этом:
Для оценки нагрузочных характеристик производственных развертываний:
Для оценки производительности при локальной разработке:
- Панель Performance в Chrome DevTools
app.config.performanceвключает маркеры производительности, специфичные для Vue, на временной шкале производительности Chrome DevTools.
- Расширение Vue DevTools также предоставляет возможность оценки производительности.
Оптимизация загрузки страниц
Существует множество аспектов оптимизации производительности загрузки страниц, не зависящих от фреймворка, - ознакомьтесь с этим руководством 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.
Общие оптимизации
Следующие советы влияют как на загрузку страницы, так и на производительность обновления.
Виртуализация больших списков
Одной из наиболее распространенных проблем производительности во всех фронтенд-приложениях является рендеринг больших списков. Каким бы производительным ни был фреймворк, рендеринг списка с тысячами элементов будет медленным из-за огромного количества узлов 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 элементов, в котором каждый компонент элемента содержит множество дочерних компонентов. Удаление одной ненужной абстракции компонента здесь может привести к сокращению сотен экземпляров компонентов.