автор:маленький прозрачный фронтиз Thunder Front End
Оригинальный адрес:Поваренная книга: оптимизация производительности компонентов Vue во время выполнения
предисловие
Vue 2.0 известен своей отличной производительностью во время выполнения с момента своего выпуска.Вы можете использовать этосторонний бенчмаркдля сравнения производительности других фреймворков. Vue использует виртуальный DOM для рендеринга представления.При изменении данных Vue сравнивает два дерева компонентов до и после и синхронизирует только необходимые обновления представления.
Vue многое делает за нас, но для некоторых сложных сценариев, особенно для рендеринга большого объема данных, мы всегда должны обращать внимание на производительность приложения во время выполнения.
Эта статья создана по образцуVue CookbookОрганизовано для иллюстрации оптимизации производительности компонентов Vue во время выполнения.
основной пример
В следующем примере мы разработали элемент управления деревом, который поддерживает отображение базовой древовидной структуры, а также расширение и свертывание узлов.
Мы определяем интерфейс компонента Tree следующим образом.data
Данные, привязанные к элементу управления деревом, представляют собой массив из нескольких деревьев.children
Представляет дочерний узел.expanded-keys
привязан к расширенному узлуkey
свойства, использованиеsync
Модификатор для синхронизации обновлений состояния расширения узла, запускаемых внутри компонента.
<template>
<tree :data="data" expanded-keys.sync="expandedKeys"></tree>
</template>
<script>
export default {
data() {
return {
data: [{
key: '1',
label: '节点 1',
children: [{
key: '1-1',
label: '节点 1-1'
}]
}, {
key: '2',
label: '节点 2'
}]
}
}
};
</script>
Реализация компонента Tree выглядит следующим образом: это немного более сложный пример, чтение которого займет несколько минут.
<template>
<ul class="tree">
<li
v-for="node in nodes"
v-show="status[node.key].visible"
:key="node.key"
class="tree-node"
:style="{ 'padding-left': `${node.level * 16}px` }"
>
<i
v-if="node.children"
class="tree-node-arrow"
:class="{ expanded: status[node.key].expanded }"
@click="changeExpanded(node.key)"
>
</i>
{{ node.label }}
</li>
</ul>
</template>
<script>
export default {
props: {
data: Array,
expandedKeys: {
type: Array,
default: () => [],
},
},
computed: {
// 将 data 转为一维数组,方便 v-for 进行遍历
// 同时添加 level 和 parent 属性
nodes() {
return this.getNodes(this.data);
},
// status 是一个 key 和节点状态的一个 Map 数据结构
status() {
return this.getStatus(this.nodes);
},
},
methods: {
// 对 data 进行递归,返回一个所有节点的一维数组
getNodes(data, level = 0, parent = null) {
let nodes = [];
data.forEach((item) => {
const node = {
level,
parent,
...item,
};
nodes.push(node);
if (item.children) {
const children = this.getNodes(item.children, level + 1, node);
nodes = [...nodes, ...children];
node.children = children.filter(child => child.level === level + 1);
}
});
return nodes;
},
// 遍历 nodes,计算每个节点的状态
getStatus(nodes) {
const status = {};
nodes.forEach((node) => {
const parentStatus = status[node.parent && node.parent.key] || {};
status[node.key] = {
expanded: this.expandedKeys.includes(node.key),
visible: node.level === 0 || (parentStatus.expanded && parentStatus.visible),
};
});
return status;
},
// 切换节点的展开状态
changeExpanded(key) {
const index = this.expandedKeys.indexOf(key);
const expandedKeys = [...this.expandedKeys];
if (index >= 0) {
expandedKeys.splice(index, 1);
} else {
expandedKeys.push(key);
}
this.$emit('update:expandedKeys', expandedKeys);
},
},
};
</script>
При расширении или сворачивании узла мы просто обновляемexpanded-keys
,status
Вычисляемые свойства автоматически обновляются, чтобы обеспечить правильное видимое состояние связанных дочерних узлов.
Все готово, для измерения производительности компонента Tree задаем две метрики.
- время первого рендера
- Время развертывания/свертывания узла
Добавьте следующий код в компонент Tree, используйтеconsole.time
иconsole.timeEnd
Конкретные затраты времени на операцию могут быть выведены.
export default {
// ...
methods: {
// ...
changeExpanded(key) {
// ...
this.$emit('update:expandedKeys', expandedKeys);
console.time('expanded change');
this.$nextTick(() => {
console.timeEnd('expanded change');
});
},
},
beforeCreate() {
console.time('first rendering');
},
mounted() {
console.timeEnd('first rendering');
},
};
Между тем, чтобы усилить возможные проблемы с производительностью, мы написали метод для создания контролируемого объема данных узла.
<template>
<tree :data="data" :expanded-keys.sync="expandedKeys"></tree>
</template>
<script>
export default {
data() {
return {
// 生成一个有 3 层,每层 10 个共 1000 个节点的节点树
data: this.getRandomData(3, 10),
expandedKeys: [],
};
},
methods: {
getRandomData(layers, count, parent) {
return Array.from({ length: count }, (v, i) => {
const key = (parent ? `${parent.key}-` : '') + (i + 1);
const node = {
key,
label: `节点 ${key}`,
};
if (layers > 1) {
node.children = this.getRandomData(layers - 1, count, node);
}
return node;
});
},
},
};
<script>
ты можешь пройти этоПолный пример CodeSandboxДавайте на самом деле наблюдать за потерей производительности. Щелкните стрелку, чтобы развернуть или свернуть узел, и вывод в консоли Chrome DevTools (не используйте консоль CodeSandbox, это не точно) выглядит следующим образом.
first rendering: 406.068115234375ms
expanded change: 231.623779296875ms
На маломощном ноутбуке автора первоначальный рендеринг занимает 400+ мс, а развертывание или свертывание узлов — 200+ мс. Давайте оптимизируем производительность компонента Tree.
Если ваше устройство имеет высокую производительность, вы можете изменить количество сгенерированных узлов, например
this.getRandomData(4, 10)
Создайте 10000 узлов.
Найдите узкие места в производительности с помощью Chrome Performance
Панель «Производительность» в Chrome может записывать данные и время выполнения js за определенный период времени. Шаги для анализа производительности страницы с помощью Chrome DevTools следующие.
- Откройте Инструменты разработчика Chrome и переключитесь на панель «Производительность».
- Нажмите «Запись», чтобы начать запись
- Обновите страницу или разверните узел
- Нажмите «Стоп», чтобы остановить запись.
console.time
Выходное значение также отображается в Performance, чтобы помочь нам в отладке. Подробнее о производительности можнонажмите здесь, чтобы посмотреть.
Оптимизация производительности во время выполнения
условный рендеринг
Мы прокрутили вниз результаты анализа производительности и обнаружили, что большая часть времени уходит на функцию рендеринга, а внизу есть много других вызовов функций.
При обходе узла для видимости узла используемv-show
невидимые узлы также визуализируются, а затем стилизуются, чтобы сделать их невидимыми. Так что попробуйте использоватьv-if
инструкции для условного рендеринга.
<li
v-for="node in nodes"
v-if="status[node.key].visible"
:key="node.key"
class="tree-node"
:style="{ 'padding-left': `${node.level * 16}px` }"
>
...
</li>
v-if
Выражается в виде троичного выражения в функции рендеринга:
visible ? h('li') : this._e() // this._e() 生成一个注释节点
которыйv-if
Простое сокращение времени каждого обхода не уменьшает количество обходов. иРуководство по стилю Vue.jsчетко указано, что нельзяv-if
иv-for
Используется для одного и того же элемента в одно и то же время, так как это может привести к ненужному отображению.
Вместо этого мы можем перебрать вычисленные свойства видимого узла:
<li
v-for="node in visibleNodes"
:key="node.key"
class="tree-node"
:style="{ 'padding-left': `${node.level * 16}px` }"
>
...
</li>
<script>
export {
// ...
computed: {
visibleNodes() {
return this.nodes.filter(node => this.status[node.key].visible);
},
},
// ...
}
</script>
Оптимизированное время работы выглядит следующим образом.
first rendering: 194.7890625ms
expanded change: 204.01904296875ms
ты можешь пройтиУлучшенный пример (Demo2)Чтобы наблюдать потерю производительности компонентов, есть большое улучшение по сравнению с до оптимизации.
двусторонняя привязка
В предыдущем примере мы использовали.sync
правильноexpanded-keys
Выполняется «двусторонняя привязка», которая на самом деле является синтаксическим сахаром для реквизита и пользовательских событий. Таким образом, родительскому компоненту Дерева очень удобно синхронизировать обновление развернутого состояния.
Однако при использовании компонента "Дерево" неexpanded-keys
, приведет к тому, что узел не сможет расшириться или свернуться, даже если вы не заботитесь о расширении или свертывании. положи сюдаexpanded-keys
как внешний побочный эффект.
<!-- 无法展开 / 折叠节点 -->
<tree :data="data"></tree>
Здесь также есть некоторые проблемы с производительностью: при развертывании или сворачивании узла срабатывает побочный эффект обновления родительского компонента.expanded-keys
. компонента "Дерево"status
зависел отexpanded-keys
, который вызоветthis.getStatus
способ получить новыйstatus
. Даже изменение состояния одного узла приведет к пересчету состояния всех узлов.
мы считаемstatus
Как внутреннее состояние компонента Дерева, при развертывании или сворачивании узла непосредственноstatus
модифицировать. Также определите узел расширения по умолчаниюdefault-expanded-keys
.status
Зависит только от инициализацииdefault-expanded-keys
.
export default {
props: {
data: Array,
// 默认展开节点
defaultExpandedKeys: {
type: Array,
default: () => [],
},
},
data() {
return {
status: null, // status 为局部状态
};
},
computed: {
nodes() {
return this.getNodes(this.data);
},
},
watch: {
nodes: {
// nodes 改变时重新计算 status
handler() {
this.status = this.getStatus(this.nodes);
},
// 初始化 status
immediate: true,
},
// defaultExpandedKeys 改变时重新计算 status
defaultExpandedKeys() {
this.status = this.getStatus(this.nodes);
},
},
methods: {
getNodes(data, level = 0, parent = null) {
// ...
},
getStatus(nodes) {
// ...
},
// 展开或折叠节点时直接修改 status,并通知父组件
changeExpanded(key) {
console.time('expanded change');
const node = this.nodes.find(n => n.key === key); // 找到该节点
const newExpanded = !this.status[key].expanded; // 新的展开状态
// 递归该节点的后代节点,更新 status
const updateVisible = (n, visible) => {
n.children.forEach((child) => {
this.status[child.key].visible = visible && this.status[n.key].expanded;
if (child.children) updateVisible(child, visible);
});
};
this.status[key].expanded = newExpanded;
updateVisible(node, newExpanded);
// 触发节点展开状态改变事件
this.$emit('expanded-change', node, newExpanded, this.nodes.filter(n => this.status[n.key].expanded));
this.$nextTick(() => {
console.timeEnd('expanded change');
});
},
},
beforeCreate() {
console.time('first rendering');
},
mounted() {
console.timeEnd('first rendering');
},
};
При использовании компонента "Дерево", даже если он не переданdefault-expanded-keys
, узел также можно развернуть или свернуть в обычном режиме.
<!-- 节点可以展开或收起 -->
<tree :data="data"></tree>
<!-- 配置默认展开的节点 -->
<tree
:data="data"
:default-expanded-keys="['1', '1-1']"
@expanded-change="handleExpandedChange"
>
</tree>
Оптимизированное время работы выглядит следующим образом.
first rendering: 91.48193359375ms
expanded change: 20.4287109375ms
ты можешь пройтиУлучшенный пример (Demo3)наблюдать за потерей производительности компонентов.
Заморозить данные
До сих пор проблемы с производительностью компонента Tree не были очевидны. Чтобы еще больше расширить проблему производительности, поищите возможности для оптимизации. Мы увеличили количество узлов до 10 000.
// 生成 10000 个节点
this.getRandomData(4, 1000)
Здесь мы намеренно вносим изменение, которое может привести к проблемам с производительностью. Хотя в этом нет необходимости, когда это может помочь нам понять проблемы, которые будут представлены далее.
вычисляемое свойствоnodes
изменен наdata
изwatcher
получитьnodes
значение .
export default {
// ...
watch: {
data: {
handler() {
this.nodes = this.getNodes(this.data);
this.status = this.getStatus(this.nodes);
},
immediate: true,
},
// ...
},
// ...
};
Эта модификация не влияет на реализованную функцию, так что насчет производительности.
first rendering: 490.119140625ms
expanded change: 183.94189453125ms
Используйте инструмент «Производительность», чтобы попытаться найти узкие места в производительности.
Мы обнаружили, что вgetNodes
После вызова метода происходит длительноеproxySetter
. Это то, что Vue делает дляnodes
Свойства добавляют реактивность, позволяя Vue отслеживать изменения зависимостей.getStatus
То же самое справедливо.
Когда вы передаете простой объект JavaScript экземпляру Vue
data
option, Vue будет проходить по всем свойствам этого объекта и использовать Object.defineProperty для преобразования всех этих свойств в геттеры/сеттеры.
Чем сложнее и иерархичнее объект, тем больше времени занимает этот процесс. Когда у нас есть 1w узлов,proxySetter
время будет очень долгим.
Здесь есть проблема, мы не будемnodes
Конкретное свойство изменяется, но всякий раз, когдаdata
Пересчитайте, когда он изменится. Следовательно, вотnodes
Добавленная отзывчивость бесполезна. Так как же положить нежелательноеproxySetter
убери это? Один из способов - положитьnodes
Вернитесь к вычисляемым свойствам, которые обычно не имеют поведения при назначении. Другой способ — заморозить данные.
использоватьObject.freeze()
заморозить данные, что предотвращает изменение существующих свойств и означает, что система реагирования больше не может отслеживать изменения.
this.nodes = Object.freeze(this.getNodes(this.data));
Проверьте инструмент производительности,getNodes
После метода нетproxySetter
.
Показатели производительности следующие, и улучшение для первого рендеринга все еще значительное.
first rendering: 312.22998046875ms
expanded change: 179.59326171875ms
ты можешь пройтиУлучшенный пример (Demo4)наблюдать за потерей производительности компонентов.
Можем ли мы оптимизировать таким же образом?status
Как насчет отслеживания? Ответ нет, потому что нам нужно обновитьstatus
Значения атрибутов в (changeExpanded
). Таким образом, эта оптимизация работает только в том случае, если его свойства не будут обновлены, будут обновлены только данные всего объекта. А для данных с более сложной структурой и более глубоким уровнем эффект оптимизации более очевиден.
альтернативный план
Мы видим, что в примере много циклов или рекурсии, будь то отрисовка узлов или вычисление данных. Для такого большого объема данных, в дополнение к упомянутой выше оптимизации для Vue, мы также можем оптимизировать по двум аспектам: сократить время, затрачиваемое на каждый цикл, и уменьшить количество циклов.
Например, словарь можно использовать для оптимизации поиска данных.
// 生成 defaultExpandedKeys 的 Map 对象
const expandedKeysMap = this.defaultExpandedKeys.reduce((map, key) => {
map[key] = true;
return map;
}, {});
// 查找时
if (expandedKeysMap[key]) {
// do something
}
defaultExpandedKeys.includes
Сложность события O (n),expandedKeysMap[key]
Временная сложность O(1).
Узнайте больше об оптимизации производительности приложений Vue.Руководство по оптимизации производительности приложений Vue.
ценность этого
Производительность приложений очень важна для улучшения взаимодействия с пользователем, и ее часто упускают из виду. Представьте, что приложение, которое хорошо работает на одном устройстве, вызывает сбой в работе браузера пользователя на другом устройстве с плохой конфигурацией. Или ваше приложение нормально работает на обычных данных, но требует длительного ожидания на больших объемах данных, возможно из-за этого вы пропускаете некоторых пользователей.
Суммировать
Оптимизация производительности — актуальная тема, и не существует универсального решения для всех проблем с производительностью. Оптимизацию производительности можно проводить постоянно, но по мере углубления проблемы узкое место в производительности будет становиться все менее и менее очевидным, а оптимизация усложняться.
Пример в этой статье имеет некоторую специфику, но он помогает нам понять методологию оптимизации производительности.
- Определение метрик для измерения производительности во время выполнения
- Определите цели оптимизации, такие как достижение более 1 Вт данных в секунду.
- Анализируйте проблемы с производительностью с помощью инструмента (Chrome Performance)
- Расставьте приоритеты в большой голове (узком месте) проблемы
- Повторите 3 4 шага до достижения