Поваренная книга: оптимизация производительности компонентов Vue во время выполнения

внешний интерфейс JavaScript Chrome Vue.js
Поваренная книга: оптимизация производительности компонентов Vue во время выполнения

автор:маленький прозрачный фронтиз 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 задаем две метрики.

  1. время первого рендера
  2. Время развертывания/свертывания узла

Добавьте следующий код в компонент 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 следующие.

  1. Откройте Инструменты разработчика Chrome и переключитесь на панель «Производительность».
  2. Нажмите «Запись», чтобы начать запись
  3. Обновите страницу или разверните узел
  4. Нажмите «Стоп», чтобы остановить запись.

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 экземпляру Vuedataoption, 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. Определение метрик для измерения производительности во время выполнения
  2. Определите цели оптимизации, такие как достижение более 1 Вт данных в секунду.
  3. Анализируйте проблемы с производительностью с помощью инструмента (Chrome Performance)
  4. Расставьте приоритеты в большой голове (узком месте) проблемы
  5. Повторите 3 4 шага до достижения

Пролистайте и обратите внимание на внешний публичный аккаунт Xunlei.