[Разработка веб-сайтов, JavaScript, Программирование, VueJS] Улучшение производительности vue приложения
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
У нас в TeamHood есть wiki. Там собралась коллекция рекоммендаций, в том числе, по улучшению производительности тяжелого фронтенда на vue.js. Улучшать производительность понадобилось, потому что в силу специфики наши основные экраны не имеют пагинации. Есть клиенты, у которых на одной kanban/gantt доске больше тысячи вот таких вот карточек, все это должно работать без лагов.
В статье разобрано несколько редко упоминаемых техник из нашей wiki, которые помогут сократить излишний рендеринг компонентов и улучшить производительность.Примеры статьи собраны в отдельном репозитории. Это vue2 приложение, хотя все проверено и продолжает быть актуальным для vue3. Пока что не вся экосистема vue3 production-ready. В vuex4 утекает память, исследовать соответствующие оптимизации там пока бессмысленно (что обнадеживает, затраты памяти там в разы меньше чем в vue2+vuex3). Примеры написаны на минимальном простейшем javascript, было искушение воткнуть vue-class-component, typescript, typed-vuex и остальную кухню реального проекта, но удержался.1. (Deep) Object WatchersПравило - не использовать deep модификатор, использовать watch только для примитивных типов. Расмотрим пример. Некий массив items приходит с сервера, сохраняется в vuex store, отрисовывается, возле каждого item есть чекбокс. Свойство isChecked относится к интерфейсу, хранится отдельно от item, однако есть getter, который собирает их вместе:
export const state = () => ({
items: [{ id: 1, name: 'First' }, { id: 2, name: 'Second' }],
checkedItemIds: [1, 2]
})
export const getters = {
extendedItems (state) {
return state.items.map(item => ({
...item,
isChecked: state.checkedItemIds.includes(item.id)
}))
}
}
Допустим, items могут быть отсортированы, будем сохранять порядок. Что-то вроде:
export default class ItemList extends Vue {
computed: {
extendedItems () { return this.$store.getters.extendedItems },
itemIds () { return this.extendedItems.map(item => item.id) }
},
watch: {
itemIds () {
console.log('Saving new items order...', this.itemIds)
}
}
}
Здесь переключение чекбокса у любого item вызывается излишнее срабатывание сохранение порядка. Конструирование новых объектов - настолько естественный процесс, что даже в этом тривиальном примере мы делаем это дважды. Изменение checkedItemIds вызвает пересоздание массива extendedItems (и пересоздание каждого элемента этого массива), затем идет пересоздание объекта itemIds. Это может казаться контра-интуитивным, ведь создается массив, состоящий из тех же самых элементов в том же самом порядке. Однако, это природа javascript, [1,2,3] != [1,2,3]. Демо здесь: example1.Решение - полный отказ от использования watcher для объектов и массивов. Для каждого сложного watcher создается отдельный computed примитивного типа. Например, если требуется отслеживать свойства {id, title, userId}в массиве items, можно отслеживать строку:
computed: {
itemsTrigger () {
return JSON.stringify(items.map(item => ({
id: item.id,
title: item.title,
userId: item.userId
})))
}
},
watch: {
itemsTrigger () {
// Здесь не нужен JSON.parse - дешевле пользоваться исходным this.items;
}
}
Очевидно, чем точнее условие для срабатывания watcher, тем лучше, тем точнее он срабатывает.
Объектный watcher - плохо, deep watcher - еще хуже. Использование deep в коде - частый признак неграмотности разработчика. Типа я не понимаю что делает этот код, какими объектами он оперирует, но что-то иногда не срабатывает, навешу-ка я deep - о вроде работает.Это что-то уровня.. (был у меня и такой проект).. в компоненте не срабатывала реактивность, и вместо того, чтобы найти ошибку, был повешен $emit('reinit'), по которому родительский компонент убивал данный и создавал его заново в $nextTick. Все это забавно мигало.2. Ограничение реактивности через Object.freezeИспользование Object.freeze на проекте TeamHood сократило потребление памяти в 2 раза. Однако оно больше относится к моему второму основному проекту, StarBright, где используется nuxt и серверный рендеринг. Nuxt подразумевает, что некоторые запросы будут отрабатываться на сервере заранее. Ответы сохраняются в vuex store (и потом используются на клиенте). Таким образом, всю логику работы с запросами и кешированием данных удобнее держать в vuex. Компонент делает this.$store.dispatch('fetch', …), а vuex отдает кеш или делает запрос.Следовательно, в vuex может содержаться большой объем данных. Например, пользователь вводил адрес, autocomplete загрузил массив городов, который был закеширован в store с целью избежать повторной загрузки. Данные статичны, однако vue по умолчанию делает реактивным каждое свойство каждого объекта (рекурсивно). Во многих случаях это приводит к высокому расходу памяти, и лучше пожертвовать реактивностью отдельных свойств.
// Вместо
state: () => ({
items: []
}),
mutations: {
setItems (state, items) {
state.items = items
},
markItemCompleted (state, itemId) {
const item = state.items.find(item => item.id === itemId)
if (item) {
item.completed = true
}
}
}
// Делаем
state: () => ({
items: []
}),
mutations: {
setItems (state, items) {
state.items = items.map(item => Object.freeze(item))
},
markItemCompleted (state, itemId) {
const itemIndex = state.items.find(item => item.id === itemId)
if (itemIndex !== -1) {
// Не получится делать item.completed = true (объект заморожен), нужно пересоздать весь объект;
const newItem = {
...state.items[itemIndex],
completed: true
}
state.items.splice(itemIndex, 1, Object.freeze(newItem))
}
}
}
Пример здесь: example2. Замечу, что замерять расход памяти нужно на build-версии (не в development).3. Функциональные геттерыИногда это пропускают в документации. Функциональные геттеры не кешируются. Вот это будет делать items.find для каждого компонента:
// Vuex:
getters: {
itemById: (state) => (itemId) => state.items.find(item => item.id === itemId)
}
...
// Some <Item :item-id="itemId" /> component:
computed: {
item () { return this.$store.getters.itemById(this.itemId) }
}
Вот это построит объект itemsByIds при первом обращении и закеширует результат:
getters: {
itemByIds: (state) => state.items.reduce((out, item) => {
out[item.id] = item
return out
}, {})
}
// Some <Item :item-id="itemId" /> component:
computed: {
item () { return this.$store.getters.itemsByIds[this.itemId] }
}
Пример здесь: example3.4. Грамотное распределение на компонентыКомпоненты - ключевая часть экосистемы vue. Понимание жизненного цикла и критериев обновления (shouldComponentUpdate) необходимо для строительства эффективного приложения. Первое знакомство с компонентами проходит на интуитивно-логическом уровне: есть список каких-то однотипных контейнеров, тогда наверное для контейнера лучше сделать отдельный компонент.Однако, кроме смыслового значения, компоненты - это мощный механизм, дающий контроль над гранулярностью обновлений, это штука, напрямую влияющая на производительность. Рассмотрим такой (ужасный) код:
// Store:
export const getters = {
extendedItems (state) {
return state.items.map(item => ({
...item,
isChecked: state.checkedItemIds.includes(item.id)
}))
},
extendedItemsByIds (state, getters) {
return getters.extendedItems.reduce((out, extendedItem) => {
out[extendedItem.id] = extendedItem
return out
}, {})
}
}
// App.vue:
<ItemById for="id in $store.state.ids" :key="id" :item-id="id />
// Item.vue:
<template>
<div>{{ item.title }}</div>
</template>
<script>
export default {
props: ['itemId'],
computed: {
item () { return this.$store.getters.extendedItemsByIds[this.itemId] }
},
updated () {
console.count('Item updated')
}
}
</script>
Пример работы здесь: example4p1. Обновление любого свойства одного item вызывает обновление всех компонентов <Item>. Причина в том, что технически <Item> ссылается на объект extendedItemsByIds, который пересоздается заново при изменении любого свойства любого item. Каждый vue компонент - это функция, которая отдает virtual DOM и кеширует его (memoization). Входные аргументы функции - зависимости - отлеживаются на этапе dry run и состоят из ссылок на переменные в props и глобальные объекты типа $store. Если аргумент - поэлементно равный предыдущему новый объект, кеширование не срабатывает.Изначальная структура данных в store неудачная. Мы начали применять normalizr подход, но не доделали. Удобнее хранить сортировку в отдельном массиве ids. Так же, вместо копирования всех свойств объекта в getter, лучше просто хранить ссылку на весь объект. Например, так:
// Store:
export const state = () => ({
ids: [],
itemsByIds: {},
checkedIds: []
})
export const getters = {
extendedItems (state, getters) {
return state.ids.map(id => ({
id,
item: state.itemsByIds[id],
isChecked: state.checkedIds.includes(id)
}))
}
}
export const mutations = {
renameItem (state, { id, title }) {
const item = state.itemsByIds[id]
if (item) {
state.itemsByIds[id] = Object.freeze({
...item,
title
})
}
},
setCheckedItemById (state, { id, isChecked }) {
const index = state.checkedIds.indexOf(id)
if (isChecked && index === -1) {
state.checkedIds.push(id)
} else if (!isChecked && index !== -1) {
state.checkedIds.splice(index, 1)
}
}
}
// Item.vue:
computed: {
item () {
return this.$store.state.itemsByIds[this.itemId]
},
isChecked () {
return this.$store.state.checkedIds.includes(this.itemId)
}
}
Заметим, что мутация renameItem не перестраивает state.itemsByIds, а обновляет только один элемент оттуда. Поэтому rename работает правильно: example4p2. Однако isChecked все равно ссылается на весь state.checkedIds (ищет там значение), поэтому чекбокс по-прежнему вызывает полный ререндеринг всех <Item>.Эта ошибка уйдет, если в каждый <Item> гранулярно передать только его параметры:
<Item
v-for="extendedItem in extendedItems"
:key="extendedItem.id"
:item="extendedItem.item"
:is-checked="extendedItem.isChecked"
/>
Пример здесь: example4p3.5. Применение IntersectionObserverОтрисовка большого DOM-дерева тормозит сама по себе. Мы применяем несколько техник для оптимизации. Например, на gantt схемах размеры и положения блоков заранее расчитаны, поэтому известно, что попадает в viewport. Невидимые элементы не отрисовываются. В других случаях размеры заранее неизвестны, тогда можно применить этот простой прием с intersection observer. В vuetify есть v-intersect директива, которая работает из коробки, однако она создает отдельный IntersectionObserver на каждый свой биндинг, поэтому не подходит для случая, когда объектов много.Вот пример, который будем оптимизировать: example5. Там 100 элементов (на экране помещается 10), в каждом мигает тяжелая картинка, замеряется задержка между реальным миганием и расчетным. Создадим один экземпляр IntersectionObserver и пробросим его через директиву во все узлы, которые он будет отслеживать.Все, что нужно от директивы - зарегистрироваться в переданном IntersectionObserver:
export default {
inserted (el, { value: observer }) {
if (observer instanceof IntersectionObserver) {
observer.observe(el)
}
el._intersectionObserver = observer
},
update (el, { value: newObserver }) {
const oldObserver = el._intersectionObserver
const isOldObserver = oldObserver instanceof IntersectionObserver
const isNewObserver = newObserver instanceof IntersectionObserver
if (!isOldObserver && !isNewObserver) || (isOldObserver && (oldObserver === newObserver)) {
return false
}
if (isOldObserver) {
oldObserver.unobserve(el)
el._intersectionObserver = undefined
}
if (isNewObserver) {
newObserver.observe(el)
el._intersectionObserver = newObserver
}
},
unbind (el) {
if (el._intersectionObserver instanceof IntersectionObserver) {
el._intersectionObserver.unobserve(el)
}
el._intersectionObserver = undefined
}
}
Теперь известно, какие элементы списка не видны, вопрос, как их облегчать. Можно, например, проставить какие-то vue переменные, на основе которых тяжелый компонент будет заменяться на легкую заглушку. Однако важно понимать, что сложный компонент сложно отрисовывать. При быстром скролинге страница затупит из-за большого количества инициализаций и деинициализаций. Практика показывает, что хорошо работает скрытие на уровне css:
<template>
<div
v-for="i in 100"
:key="i"
v-node-intersect="intersectionObserver"
class="rr-intersectionable"
>
<Heavy />
</div>
</template>
<script>
export default {
data () {
return {
intersectionObserver: new IntersectionObserver(this.handleIntersections)
}
},
methods: {
handleIntersections (entries) {
entries.forEach((entry) => {
const className = 'rr-intersectionable--invisible'
if (entry.isIntersecting) {
entry.target.classList.remove(className)
} else {
entry.target.classList.add(className)
}
})
}
}
}
</script>
<style>
.rr-intersectionable--invisible .rr-heavy-part
display: none
</style>
Ссылки
- Исходный код примеров: https://kasheftin.github.io/vue-rerendering-optimization/
- Все демо: https://github.com/Kasheftin/vue-rerendering-optimization
- Caution using watchers for objects in vue
===========
Источник:
habr.com
===========
Похожие новости:
- [Программирование, C++, Работа с 3D-графикой, Разработка игр, CGI (графика)] Vulkan. Руководство разработчика. Image view (перевод)
- [Информационная безопасность, JavaScript, Google Chrome, Браузеры] Новая утечка истории браузера через favicon
- [Программирование, Visual Studio, DevOps, Микросервисы] Шаблон микросервиса: зачем нужен и как его внедрить в разработку
- [Высокая производительность, Программирование, Go] Ручное управление памятью в языке Go (перевод)
- [Assembler, Программирование микроконтроллеров] Assembler Editor Plus: Использование модулей
- [JavaScript, ООП] Создание квадратизированной галереи проектов на JS
- [Программирование, C++, Параллельное программирование] OOX 2.0: Out of Order eXecution made easy
- [Разработка веб-сайтов, Программирование, Анализ и проектирование систем, GitHub] Грубая оценка проблемности GitHub-проектов
- [Программирование, Управление разработкой, Управление проектами, Финансы в IT] О проблемах нормальной оценки фич и как их решить
- [Программирование, Разработка игр, Управление персоналом, Социальные сети и сообщества] LuxCity — стратегия для разработчиков, где код решает все
Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_programmirovanie (Программирование), #_vuejs, #_vue.js, #_optimizatsija (оптимизация), #_rendering_html (рендеринг html), #_razrabotka_vebsajtov (
Разработка веб-сайтов
), #_javascript, #_programmirovanie (
Программирование
), #_vuejs
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 06:17
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
У нас в TeamHood есть wiki. Там собралась коллекция рекоммендаций, в том числе, по улучшению производительности тяжелого фронтенда на vue.js. Улучшать производительность понадобилось, потому что в силу специфики наши основные экраны не имеют пагинации. Есть клиенты, у которых на одной kanban/gantt доске больше тысячи вот таких вот карточек, все это должно работать без лагов. В статье разобрано несколько редко упоминаемых техник из нашей wiki, которые помогут сократить излишний рендеринг компонентов и улучшить производительность.Примеры статьи собраны в отдельном репозитории. Это vue2 приложение, хотя все проверено и продолжает быть актуальным для vue3. Пока что не вся экосистема vue3 production-ready. В vuex4 утекает память, исследовать соответствующие оптимизации там пока бессмысленно (что обнадеживает, затраты памяти там в разы меньше чем в vue2+vuex3). Примеры написаны на минимальном простейшем javascript, было искушение воткнуть vue-class-component, typescript, typed-vuex и остальную кухню реального проекта, но удержался.1. (Deep) Object WatchersПравило - не использовать deep модификатор, использовать watch только для примитивных типов. Расмотрим пример. Некий массив items приходит с сервера, сохраняется в vuex store, отрисовывается, возле каждого item есть чекбокс. Свойство isChecked относится к интерфейсу, хранится отдельно от item, однако есть getter, который собирает их вместе: export const state = () => ({
items: [{ id: 1, name: 'First' }, { id: 2, name: 'Second' }], checkedItemIds: [1, 2] }) export const getters = { extendedItems (state) { return state.items.map(item => ({ ...item, isChecked: state.checkedItemIds.includes(item.id) })) } } export default class ItemList extends Vue {
computed: { extendedItems () { return this.$store.getters.extendedItems }, itemIds () { return this.extendedItems.map(item => item.id) } }, watch: { itemIds () { console.log('Saving new items order...', this.itemIds) } } } computed: {
itemsTrigger () { return JSON.stringify(items.map(item => ({ id: item.id, title: item.title, userId: item.userId }))) } }, watch: { itemsTrigger () { // Здесь не нужен JSON.parse - дешевле пользоваться исходным this.items; } } Объектный watcher - плохо, deep watcher - еще хуже. Использование deep в коде - частый признак неграмотности разработчика. Типа я не понимаю что делает этот код, какими объектами он оперирует, но что-то иногда не срабатывает, навешу-ка я deep - о вроде работает.Это что-то уровня.. (был у меня и такой проект).. в компоненте не срабатывала реактивность, и вместо того, чтобы найти ошибку, был повешен $emit('reinit'), по которому родительский компонент убивал данный и создавал его заново в $nextTick. Все это забавно мигало.2. Ограничение реактивности через Object.freezeИспользование Object.freeze на проекте TeamHood сократило потребление памяти в 2 раза. Однако оно больше относится к моему второму основному проекту, StarBright, где используется nuxt и серверный рендеринг. Nuxt подразумевает, что некоторые запросы будут отрабатываться на сервере заранее. Ответы сохраняются в vuex store (и потом используются на клиенте). Таким образом, всю логику работы с запросами и кешированием данных удобнее держать в vuex. Компонент делает this.$store.dispatch('fetch', …), а vuex отдает кеш или делает запрос.Следовательно, в vuex может содержаться большой объем данных. Например, пользователь вводил адрес, autocomplete загрузил массив городов, который был закеширован в store с целью избежать повторной загрузки. Данные статичны, однако vue по умолчанию делает реактивным каждое свойство каждого объекта (рекурсивно). Во многих случаях это приводит к высокому расходу памяти, и лучше пожертвовать реактивностью отдельных свойств. // Вместо
state: () => ({ items: [] }), mutations: { setItems (state, items) { state.items = items }, markItemCompleted (state, itemId) { const item = state.items.find(item => item.id === itemId) if (item) { item.completed = true } } } // Делаем state: () => ({ items: [] }), mutations: { setItems (state, items) { state.items = items.map(item => Object.freeze(item)) }, markItemCompleted (state, itemId) { const itemIndex = state.items.find(item => item.id === itemId) if (itemIndex !== -1) { // Не получится делать item.completed = true (объект заморожен), нужно пересоздать весь объект; const newItem = { ...state.items[itemIndex], completed: true } state.items.splice(itemIndex, 1, Object.freeze(newItem)) } } } // Vuex:
getters: { itemById: (state) => (itemId) => state.items.find(item => item.id === itemId) } ... // Some <Item :item-id="itemId" /> component: computed: { item () { return this.$store.getters.itemById(this.itemId) } } getters: {
itemByIds: (state) => state.items.reduce((out, item) => { out[item.id] = item return out }, {}) } // Some <Item :item-id="itemId" /> component: computed: { item () { return this.$store.getters.itemsByIds[this.itemId] } } // Store:
export const getters = { extendedItems (state) { return state.items.map(item => ({ ...item, isChecked: state.checkedItemIds.includes(item.id) })) }, extendedItemsByIds (state, getters) { return getters.extendedItems.reduce((out, extendedItem) => { out[extendedItem.id] = extendedItem return out }, {}) } } // App.vue: <ItemById for="id in $store.state.ids" :key="id" :item-id="id /> // Item.vue: <template> <div>{{ item.title }}</div> </template> <script> export default { props: ['itemId'], computed: { item () { return this.$store.getters.extendedItemsByIds[this.itemId] } }, updated () { console.count('Item updated') } } </script> // Store:
export const state = () => ({ ids: [], itemsByIds: {}, checkedIds: [] }) export const getters = { extendedItems (state, getters) { return state.ids.map(id => ({ id, item: state.itemsByIds[id], isChecked: state.checkedIds.includes(id) })) } } export const mutations = { renameItem (state, { id, title }) { const item = state.itemsByIds[id] if (item) { state.itemsByIds[id] = Object.freeze({ ...item, title }) } }, setCheckedItemById (state, { id, isChecked }) { const index = state.checkedIds.indexOf(id) if (isChecked && index === -1) { state.checkedIds.push(id) } else if (!isChecked && index !== -1) { state.checkedIds.splice(index, 1) } } } // Item.vue: computed: { item () { return this.$store.state.itemsByIds[this.itemId] }, isChecked () { return this.$store.state.checkedIds.includes(this.itemId) } } <Item
v-for="extendedItem in extendedItems" :key="extendedItem.id" :item="extendedItem.item" :is-checked="extendedItem.isChecked" /> export default {
inserted (el, { value: observer }) { if (observer instanceof IntersectionObserver) { observer.observe(el) } el._intersectionObserver = observer }, update (el, { value: newObserver }) { const oldObserver = el._intersectionObserver const isOldObserver = oldObserver instanceof IntersectionObserver const isNewObserver = newObserver instanceof IntersectionObserver if (!isOldObserver && !isNewObserver) || (isOldObserver && (oldObserver === newObserver)) { return false } if (isOldObserver) { oldObserver.unobserve(el) el._intersectionObserver = undefined } if (isNewObserver) { newObserver.observe(el) el._intersectionObserver = newObserver } }, unbind (el) { if (el._intersectionObserver instanceof IntersectionObserver) { el._intersectionObserver.unobserve(el) } el._intersectionObserver = undefined } } <template>
<div v-for="i in 100" :key="i" v-node-intersect="intersectionObserver" class="rr-intersectionable" > <Heavy /> </div> </template> <script> export default { data () { return { intersectionObserver: new IntersectionObserver(this.handleIntersections) } }, methods: { handleIntersections (entries) { entries.forEach((entry) => { const className = 'rr-intersectionable--invisible' if (entry.isIntersecting) { entry.target.classList.remove(className) } else { entry.target.classList.add(className) } }) } } } </script> <style> .rr-intersectionable--invisible .rr-heavy-part display: none </style>
=========== Источник: habr.com =========== Похожие новости:
Разработка веб-сайтов ), #_javascript, #_programmirovanie ( Программирование ), #_vuejs |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 06:17
Часовой пояс: UTC + 5