В этой статье собрано девять советов о том как повысить производительность вашего приложения на Vue, увеличить скорость отображения и уменьшить размер бандла.
Предположим у нас есть простой, маленький компонент. Всё что он делает — отображает тот или иной тег в зависимости от переданного значения:
Этот компонент можно оптимизировать, добавив атрибут functional. Функциональный компонент компилируется в простую функцию и не имеет локального состояния. За счет этого его производительность намного выше:
Пример того, как могут выглядеть функциональные компоненты во Vue v3.0
Представим, компонент, для отображения которого необходимо выполнить какую-то сложную задачу:
Проблема тут в том, что Vue будет выполнять heavy() метод каждый раз при ре-рендеринге компонента, то есть каждый раз при изменении значений props.
Мы можем легко оптимизировать такой компонент, если выделим тяжелый метод в дочерний компонент:
Зачем? Vue автоматически пропускает рендеринг компонентов в которых не изменялись зависимые данные. Таким образом при изменении props в родительском компоненте дочерний будет пере использоваться и метод heavy() не будет запускаться повторно.
Учтите, что это имеет смысл только в том случае, если дочерний компонент не будет иметь зависимости от данных в родительском компоненте. Иначе вместе с родительским будет пере создаваться и дочерний и тогда данная оптимизация не имеет смысла.
Следующий компонент имеет какое-то вычисляемое свойство на основе второго вычисляемого свойства:
Важно здесь то, что свойство base вызывается в цикле, что приводит к осложнениям. Каждый раз обращаясь к реактивным данным Vue запускает некоторую логику, чтобы определить как и к каким данным вы обращаетесь построить зависимости и так далее. Эти небольшие накладные расходы суммируются если обращений много, как в нашем примере.
Чтобы исправить это достаточно просто обратится к base один раз и сохранить значение в локальную переменную:
Взгляните на следующий пример:
Здесь у нас есть компонент-обертка, который использует v-if и v-else для переключения каких-то дочерних компонентов.
Здесь важно понимать как работает v-if. Каждый раз при переключении состояния один дочерний компонент будет полностью уничтожаться (вызовется хук destroyed(), все ноды удалятся из DOM), а второй полностью создаваться и монтироваться заново. А если эти компоненты «тяжелые» то вы получите подвисание интерфейса в момент переключения.
Более производительное решение — использовать v-show
В этом случае оба дочерних компонента будут созданы и смонтированы сразу и существовать одновременно. Таким образом Vue нет нужды уничтожать и создавать компоненты при переключении. Всё что он делает — скрывает один и показывает второй средствами CSS. Так переключение состояния будет намного быстрее, но стоит понимать что это приведет к большим расходам памяти.
Итак, простой компонент — компонент-обертка над роутером.
Проблема аналогична предыдущему примеру — все компоненты в вашем роутере будут создаваться, монтироваться и уничтожаться при переходах между маршрутами.
И решение здесь аналогичное — сказать Vue не уничтожать а кэшировать и переиспользовать компоненты. Сделать это можно используя специальный встроенный компонент <keep-alive>:
Такая оптимизация приведёт к повышенному потреблению памяти, так как Vue будет поддерживать больше компонентов «живыми». Поэтому не стоит применять такой подход бездумно для всех маршрутов, особенно если у вас их много. Вы можете использовать атрибуты include и exclude для чтобы установить правила какие маршруты нужно кэшировать а какие нет:
Следующий пример — компонент, который имеет несколько очень тяжелых дочерних компонентов:
Проблема в том, что обработка компонентов это синхронная операция в основном потоке. И в данном примере браузер не отобразит ничего пока не закончится обработка всех компонентов, как родительского так и дочерних. А если на обработку дочерних нужно много времени, то вы получите лаги в интерфейсе или пустой экран на какое-то время.
Улучшить ситуацию можно отложив рендеринг дочерних компонентов:
Здесь функция defer(n) возвращает false n фреймов. после чего всегда возвращает true. Она используется чтобы отложить обработку части шаблона на несколько кадров, дав тем самым браузеру возможность отрисовать интерфейс.
Как это работает. Как я писал выше если условие в директиве v-if ложно — то Vue полностью игнорирует часть шаблона.
При первом кадре анимации мы получим:
Vue просто смонтирует заголовок, а браузер его отобразит и запросит второй кадр. В этот момент функция defer(2) начнет возвращать true
Vue создаст 10 дочерних компонентов и смонтирует их. Браузер их отобразит и запросит следующий кадр, при котором уже defer(3) вернет true.
Таким образом мы создали тяжелый компонент, который обрабатывается постепенно, давая браузеру возможность отобразить уже смонтированные части шаблона, что значительно улучшит UX так как интерфейс не будет выглядеть зависшим.
Код Defer миксина:
Теперь поговорим об импорте дочерних компонентов:
При традиционном импорте дочерний компонент загружается сразу, как браузер дойдет до инструкции import. Или, если вы используете сборщик, ваш дочерний компонент будет включен в состав общего бандла.
Однако, если ваш дочерний компонент очень большой то имеет смысл загружать его асинхронно.
Сделать это очень просто:
Это всё что нужно. Vue умеет работать с ленивой загрузкой компонентов из коробки. Он сам загрузит компонент когда тот понадобится и отобразит его как только тот будет готов. Вы можете использовать такой подход для ленивой загрузки где угодно.
Если вы используете сборщик, то всё что касается Heavy.js будет вынесено в отдельный файл. Тем самым вы уменьшите вес файлов при первоначальной загрузке ии увеличите скорость отображения.
Ленивая загрузка очень полезна в случае компонентов используемых для маршрутов. Так как вам не нужно загружать все компоненты для маршрутов одновременно я рекомендую всегда использовать такой подход:
Каждый из этих компонентов будет загружен только тогда, когда пользователь впервые запросит указанный маршрут. И не раньше.
Точно так же легко можно использовать ленивую загрузку с динамическими компонентами:
Опять таки, каждый компонент будет загружаться только тогда, когда он понадобится.
Представим, что вам нужно сохранить в хранилище большой массив данных:
Проблема вот в чем. Все комиты — синхронные операции. Это значит, что обработка большого массива заблокирует ваш интерфейс на всё время работы.
Для решения этой проблемы вы можете разбить массив на части и добавлять их поочередно, давая браузеру время на отрисовку нового кадра:
Чем более мелкими партиями вы будете добавлять данные в хранилище тем более плавным останется ваш интерфейс и тем больше суммарное время на выполнение задачи.
Кроме того, с таким подходом вы имеете возможность отобразить индикатор загрузки для пользователя. Что тоже значительно улучшит его опыт работы с приложением.
И последний на сегодня пример. Имеем похожую задачу: мы добавляем в хранилище массив очень больших объектов с кучей уровней вложенности:
Дело в том, что Vue будет выполнять рекурсивный обход всех вложенный полей и делать их реактивными.Если у вас много данных это может быть затратно, но что куда важнее — ненужно.
Если ваше приложение построено так, что зависит только от объекта верхнего уровня и не ссылается на реактивные данные где-то на несколько уровней ниже, то вы можете отключить реактивность, тем самым избавив Vue от кучи лишней работы:
Работа с компонентами
Функциональные компоненты
Предположим у нас есть простой, маленький компонент. Всё что он делает — отображает тот или иной тег в зависимости от переданного значения:
<template>
<div>
<div v-if="value"></div>
<section v-else></section>
</div>
</template>
<script>
export default {
props: ['value']
}
</script>
Этот компонент можно оптимизировать, добавив атрибут functional. Функциональный компонент компилируется в простую функцию и не имеет локального состояния. За счет этого его производительность намного выше:
<template functional>
<div>
<div v-if="props.value"></div>
<section v-else></section>
</div>
</template>
<script>
export default {
props: ['value']
}
</script>
Пример того, как могут выглядеть функциональные компоненты во Vue v3.0
Разделение на дочерние компоненты
Представим, компонент, для отображения которого необходимо выполнить какую-то сложную задачу:
<template>
<div :style="{ opacity: number / 300 }">
<div>{{ heavy() }}</div>
</div>
</template>
<script>
export default {
props: ['number'],
methods: {
heavy () { /* Тяжелая задача */ }
}
}
</script>
Проблема тут в том, что Vue будет выполнять heavy() метод каждый раз при ре-рендеринге компонента, то есть каждый раз при изменении значений props.
Мы можем легко оптимизировать такой компонент, если выделим тяжелый метод в дочерний компонент:
<template>
<div :style="{ opacity: number / 300 }">
<ChildComp/>
</div>
</template>
<script>
export default {
props: ['number'],
components: {
ChildComp: {
methods: {
heavy () { /* Тяжелая задача */ }
},
render (h) {
return h('div', this.heavy())
}
}
}
}
</script>
Зачем? Vue автоматически пропускает рендеринг компонентов в которых не изменялись зависимые данные. Таким образом при изменении props в родительском компоненте дочерний будет пере использоваться и метод heavy() не будет запускаться повторно.
Учтите, что это имеет смысл только в том случае, если дочерний компонент не будет иметь зависимости от данных в родительском компоненте. Иначе вместе с родительским будет пере создаваться и дочерний и тогда данная оптимизация не имеет смысла.
Локальный кэш геттеров
Следующий компонент имеет какое-то вычисляемое свойство на основе второго вычисляемого свойства:
<template>
<div :style="{ opacity: start / 300 }">{{ result }}</div>
</template>
<script>
export default {
props: ['start'],
computed: {
base () { return 42 },
result () {
let result = this.start
for (let i = 0; i < 1000; i++) {
result += this.base
}
return result
}
}
}
</script>
Важно здесь то, что свойство base вызывается в цикле, что приводит к осложнениям. Каждый раз обращаясь к реактивным данным Vue запускает некоторую логику, чтобы определить как и к каким данным вы обращаетесь построить зависимости и так далее. Эти небольшие накладные расходы суммируются если обращений много, как в нашем примере.
Чтобы исправить это достаточно просто обратится к base один раз и сохранить значение в локальную переменную:
<template>
<div :style="{ opacity: start / 300 }">{{ result }}</div>
</template>
<script>
export default {
props: ['start'],
computed: {
base () { return 42 },
result () {
const base = this.base // <--
let result = this.start
for (let i = 0; i < 1000; i++) {
result += base
}
return result
}
}
}
</script>
Повторное использование DOM с v-show
Взгляните на следующий пример:
<template functional>
<div>
<div v-if="props.value">
<Heavy :n="10000"/>
</div>
<section v-else>
<Heavy :n="10000"/>
</section>
</div>
</template>
Здесь у нас есть компонент-обертка, который использует v-if и v-else для переключения каких-то дочерних компонентов.
Здесь важно понимать как работает v-if. Каждый раз при переключении состояния один дочерний компонент будет полностью уничтожаться (вызовется хук destroyed(), все ноды удалятся из DOM), а второй полностью создаваться и монтироваться заново. А если эти компоненты «тяжелые» то вы получите подвисание интерфейса в момент переключения.
Более производительное решение — использовать v-show
<template functional>
<div>
<div v-show="props.value">
<Heavy :n="10000"/>
</div>
<section v-show="!props.value">
<Heavy :n="10000"/>
</section>
</div>
</template>
В этом случае оба дочерних компонента будут созданы и смонтированы сразу и существовать одновременно. Таким образом Vue нет нужды уничтожать и создавать компоненты при переключении. Всё что он делает — скрывает один и показывает второй средствами CSS. Так переключение состояния будет намного быстрее, но стоит понимать что это приведет к большим расходам памяти.
Используйте <keep-alive>
Итак, простой компонент — компонент-обертка над роутером.
<template>
<div id="app">
<router-view/>
</div>
</template>
Проблема аналогична предыдущему примеру — все компоненты в вашем роутере будут создаваться, монтироваться и уничтожаться при переходах между маршрутами.
И решение здесь аналогичное — сказать Vue не уничтожать а кэшировать и переиспользовать компоненты. Сделать это можно используя специальный встроенный компонент <keep-alive>:
<template>
<div id="app">
<keep-alive>
<router-view/>
</keep-alive>
</div>
</template>
Такая оптимизация приведёт к повышенному потреблению памяти, так как Vue будет поддерживать больше компонентов «живыми». Поэтому не стоит применять такой подход бездумно для всех маршрутов, особенно если у вас их много. Вы можете использовать атрибуты include и exclude для чтобы установить правила какие маршруты нужно кэшировать а какие нет:
<template>
<div id="app">
<keep-alive include="home,some-popular-page">
<router-view/>
</keep-alive>
</div>
</template>
Отложенный рендеринг
Следующий пример — компонент, который имеет несколько очень тяжелых дочерних компонентов:
<template>
<div>
<h3>I'm an heavy page</h3>
<Heavy v-for="n in 10" :key="n"/>
<Heavy class="super-heavy" :n="9999999"/>
</div>
</template>
Проблема в том, что обработка компонентов это синхронная операция в основном потоке. И в данном примере браузер не отобразит ничего пока не закончится обработка всех компонентов, как родительского так и дочерних. А если на обработку дочерних нужно много времени, то вы получите лаги в интерфейсе или пустой экран на какое-то время.
Улучшить ситуацию можно отложив рендеринг дочерних компонентов:
<template>
<div>
<h3>I'm an heavy page</h3>
<template v-if="defer(2)">
<Heavy v-for="n in 10" :key="n"/>
</template>
<Heavy v-if="defer(3)" class="super-heavy" :n="9999999"/>
</div>
</template>
<script>
import Defer from '@/mixins/Defer'
export default {
mixins: [
Defer()
]
}
</script>
Здесь функция defer(n) возвращает false n фреймов. после чего всегда возвращает true. Она используется чтобы отложить обработку части шаблона на несколько кадров, дав тем самым браузеру возможность отрисовать интерфейс.
Как это работает. Как я писал выше если условие в директиве v-if ложно — то Vue полностью игнорирует часть шаблона.
При первом кадре анимации мы получим:
<template>
<div>
<h3>I'm an heavy page</h3>
<template v-if="false">
<Heavy v-for="n in 10" :key="n"/>
</template>
<Heavy v-if="false" class="super-heavy" :n="9999999"/>
</div>
</template>
Vue просто смонтирует заголовок, а браузер его отобразит и запросит второй кадр. В этот момент функция defer(2) начнет возвращать true
<template>
<div>
<h3>I'm an heavy page</h3>
<template v-if="true">
<Heavy v-for="n in 10" :key="n"/>
</template>
<Heavy v-if="false" class="super-heavy" :n="9999999"/>
</div>
</template>
Vue создаст 10 дочерних компонентов и смонтирует их. Браузер их отобразит и запросит следующий кадр, при котором уже defer(3) вернет true.
Таким образом мы создали тяжелый компонент, который обрабатывается постепенно, давая браузеру возможность отобразить уже смонтированные части шаблона, что значительно улучшит UX так как интерфейс не будет выглядеть зависшим.
Код Defer миксина:
export default function (count = 10) {
return {
data () {
return {
displayPriority: 0
}
},
mounted () {
this.runDisplayPriority()
},
methods: {
runDisplayPriority () {
const step = () => {
requestAnimationFrame(() => {
this.displayPriority++
if (this.displayPriority < count) {
step()
}
})
}
step()
},
defer (priority) {
return this.displayPriority >= priority
}
}
}
}
Ленивая загрузка компонентов
Теперь поговорим об импорте дочерних компонентов:
import Heavy from 'Heavy.js'
export default {
components: { Heavy }
}
При традиционном импорте дочерний компонент загружается сразу, как браузер дойдет до инструкции import. Или, если вы используете сборщик, ваш дочерний компонент будет включен в состав общего бандла.
Однако, если ваш дочерний компонент очень большой то имеет смысл загружать его асинхронно.
Сделать это очень просто:
const Heavy = () => import('Heavy.js')
export default {
components: { Heavy }
}
Это всё что нужно. Vue умеет работать с ленивой загрузкой компонентов из коробки. Он сам загрузит компонент когда тот понадобится и отобразит его как только тот будет готов. Вы можете использовать такой подход для ленивой загрузки где угодно.
Если вы используете сборщик, то всё что касается Heavy.js будет вынесено в отдельный файл. Тем самым вы уменьшите вес файлов при первоначальной загрузке ии увеличите скорость отображения.
Ленивая загрузка Views
Ленивая загрузка очень полезна в случае компонентов используемых для маршрутов. Так как вам не нужно загружать все компоненты для маршрутов одновременно я рекомендую всегда использовать такой подход:
const Home = () => import('Home.js')
const About = () => import('About.js')
const Contacts = () => import('Contacts.js')
new VueRouter({
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/contacts', component: Contacts },
]
})
Каждый из этих компонентов будет загружен только тогда, когда пользователь впервые запросит указанный маршрут. И не раньше.
Динамические компоненты
Точно так же легко можно использовать ленивую загрузку с динамическими компонентами:
<template>
<div>
<component :is="componentToShow"/>
</div>
</template>
<script>
export default {
props: ['value'],
computed: {
componentToShow() {
if (this.value) {
return () => import('TrueComponent.js')
}
return () => import('FalseComponent.js')
}
}
}
</script>
Опять таки, каждый компонент будет загружаться только тогда, когда он понадобится.
Работа с Vuex
Избегайте больших комитов
Представим, что вам нужно сохранить в хранилище большой массив данных:
fetchItems ({ commit }, { items }) {
commit('clearItems')
commit('addItems', items) // Массив из 10000 елементов
}
Проблема вот в чем. Все комиты — синхронные операции. Это значит, что обработка большого массива заблокирует ваш интерфейс на всё время работы.
Для решения этой проблемы вы можете разбить массив на части и добавлять их поочередно, давая браузеру время на отрисовку нового кадра:
fetchItems ({ commit }, { items, splitCount }) {
commit('clearItems')
const queue = new JobQueue()
splitArray(items, splitCount).forEach(
chunk => queue.addJob(done => {
// Комитим части массива за несколько кадров
requestAnimationFrame(() => {
commit('addItems', chunk)
done()
})
})
)
// Запускаем очередь и ждем окончания
await queue.start()
}
Чем более мелкими партиями вы будете добавлять данные в хранилище тем более плавным останется ваш интерфейс и тем больше суммарное время на выполнение задачи.
Кроме того, с таким подходом вы имеете возможность отобразить индикатор загрузки для пользователя. Что тоже значительно улучшит его опыт работы с приложением.
Отключайте реактивность там где она не нужна
И последний на сегодня пример. Имеем похожую задачу: мы добавляем в хранилище массив очень больших объектов с кучей уровней вложенности:
const data = items.map(
item => ({
id: uid++,
data: item, // <-- большой многоуровневый объект
vote: 0
})
)
Дело в том, что Vue будет выполнять рекурсивный обход всех вложенный полей и делать их реактивными.Если у вас много данных это может быть затратно, но что куда важнее — ненужно.
Если ваше приложение построено так, что зависит только от объекта верхнего уровня и не ссылается на реактивные данные где-то на несколько уровней ниже, то вы можете отключить реактивность, тем самым избавив Vue от кучи лишней работы:
const data = items.map(
item => optimizeItem(item)
)
function optimizeItem (item) {
const itemData = {
id: uid++,
vote: 0
}
Object.defineProperty(itemData, 'data', {
// Отмечаем поле как "не-реактивное"
configurable: false,
value: item
})
return itemData
}