9 советов по повышению производительности Vue

  • Tutorial
В этой статье собрано девять советов о том как повысить производительность вашего приложения на 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
}

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 19

    0
    Я павильно понимаю, что это первод вот этого выступления с незначительными изменениями кода?
    www.vuemastery.com/conferences/vueconf-us-2019/9-performance-secrets-revealed
      0

      Примеры это сборная солянка из нескольких докладов. Пожалуй следует найти и добавить ссылки.

      0
      Стоит отметить, что v-show не подойдет, если компонентов одновременно будет много, но они будет достаточно легкими (большое кол-во DOM-элементов на пользу не будет).
      Также keep-alive не подойдет для случая, когда один и тот же компонент используется с разными данными, по крайней мере не без костылей (до сих пор issue висит открытым, чтобы можно было использовать key в include).
        0
        Также keep-alive не подойдет для случая, когда один и тот же компонент используется с разными данными, по крайней мере не без костылей (до сих пор issue висит открытым, чтобы можно было использовать key в include).

        Я честно говоря не совсем понял что имелось ввиду в статье. Дело в том, что vue-router по-дефолту ведет себя лениво и не пересоздает вьюхи, а реюзает их. Чтобы он пересоздавал нужно ему `key` указывать. stackoverflow.com/questions/52847979/what-is-router-view-key-route-fullpath см. ответ
          0

          Вот простой пример:



          Вы можете увидеть какие хуки жизненного цикла вызываются при переходе между маршрутами. Каждый компонент создаётся перед переходом, и разрушается после перехода.


          Сравните это со вторым примером:



          Здесь вы увидите, что каждый компонент создаётся перед первым переходом, но не разрушается. И не создаётся заново при повторном переходе. Компонент сохраняет своё состояние когда вы уходите на другой маршрут.

            0
            Да, верно. Мой кейс, видимо надо уточнить — дело касается переходов по маршрутам, использующим один и тот же компонент
            codepen.io/mbeloshitsky/pen/povLRey
        0
        Object.defineProperty(itemData, 'data', {
            // Отмечаем поле как "не-реактивное"
            configurable: false,
            value: item
        })

        не проще ли воспользоваться Object.freeze?
          0
          Object.freeze работет только поверхностно и не затронет вложенные объекты
            0
            Верно, как и пример в статье.
          0
          Геттер следует кешировать, если он возвращает массив, и обращаемся к нему через .map, .filter или .forEach?
          computed: {
            arr() { return [1, 2, 3, 4, 5] },
            arr2() { return this.arr.map(x => x*2) },
          }
          
            +1

            Нет. Вычисляемое свойство имеет смысл кэшировать только тогда когда оно вызывается в цикле. В вашем случае this.arr будет вызываться только один раз.

            0
            За подборку спасибо, но стоило расписать всё поподробнее. Несколько моментов для примера:

            Функциональный компонент компилируется в простую функцию и не имеет локального состояния. За счет этого его производительность намного выше
            А по ссылке написано всё наоборот. Вы тогда внесите ясность, типа «сейчас надо делать так, но в следующей версии всё поменяется и будет по-другому».

            Каждый раз обращаясь к реактивным данным Vue запускает некоторую логику, чтобы определить как и к каким данным вы обращаетесь построить зависимости и так далее.
            При инициализации системы реактивности и при вызове сеттера он что-то такое делает, да. А при простом обращении почему?

            Представим, что вам нужно сохранить в хранилище большой массив данных

            Это значит, что обработка большого массива заблокирует ваш интерфейс на всё время работы.
            Когда мы от добавления успели перейти к обработке? И почему обработка заблокирует интерфейс? В .Net, к примеру, я спокойно могу написать в UI-потоке что-то вроде
            for (int i = 0; i < 1000000; i++) {arr[i] += i;}
            и ничего нигде не заблокируется, даже не лагнёт.
            Также непонятно, на чём основано решение, JobQueue и requestAnimationFrame — это какие-то стандартные вещи или нет, что они делают?

            Имеем похожую задачу: мы добавляем в хранилище массив очень больших объектов с кучей уровней вложенности
            Тут проблема относится к Vue, а не конкретно Vuex.
            Что вообще за «configurable: false» и как оно работает? Гугл выдаёт feature request, который непонятно чем закончился. И код — это конечно хорошо, но ещё лучше словами пояснить, что происходит.
              0
              А по ссылке написано всё наоборот. Вы тогда внесите ясность, типа «сейчас надо делать так, но в следующей версии всё поменяется и будет по-другому».

              В данный момент функциональный компонент это объект с полем рендер функции


              export default {
                functional: true,
                render(createElement, context) {
                  return createElement('button', 'Click me');
                }
              };

              В следующей версии он будет упрощен до обычной рендер-функции


              const FunctionalComp = props => {
                const theme = inject(themeSymbol)
                return h('div', `Using theme ${theme}`)
              }

              Не совсем уверен, что вы имели в виду говоря "всё наоборот".


              При инициализации системы реактивности и при вызове сеттера он что-то такое делает, да. А при простом обращении почему?

              Как я и написал — чтобы построить дерево зависимостей. Другими словами он вызывает гетеры у реактивных свойств, и запоминает что при вычислении result было вызвано свойство base, а значит при изменении base нужно повторно вычислить result.


              И почему обработка заблокирует интерфейс?

              Потому что так работает JavaScript. В JavaScript нет многопоточности. Если упростить, то вся ваша вкладка работает в одном потоке. Это значит что никакие две задачи не могут исполняться параллельно. И если вы запустите какую-то долгую задачу на JS, то всё остальное будет заблокировано. Попробуйте на любой странице запустить долгий цикл, например:


              for (let i = 0; i < Number.MAX_SAFE_INTEGER; i++) {}

              И пока он выполняется попробуйте взаимодействовать со страницей: понажимать кнопки, ссылки, какой-нибудь слайдер попереключать.


              Также непонятно, на чём основано решение, JobQueue и requestAnimationFrame — это какие-то стандартные вещи или нет, что они делают?

              window.requestAnimationFrame() — MDN
              splitArray — абстрактная функция которая разбивает массив на части указанного размера и возвращает массив масивов.
              JobQueue — абстрактный класс, который принимает какие-то колбеки (задания), и выполняет их один за одним.
              Различных реализаций в интернете масса.


              Что вообще за «configurable: false» и как оно работает? Гугл выдаёт feature request, который непонятно чем закончился

              Object.defineProperty() — MDN


              Или как указали в коментариях выше, можно использовать Object.freeze()

                0
                Всё наоборот, в смысле "{ functional: true } option removed" и "Functional components in v3 should be used primarily for simplicity, not performance". Т.е., насколько я понимаю из описания, не будет никаких функциональных компонентов и не будут они улучшать производительность, но можно использовать функции, если вдруг есть такое желание.

                Как я и написал — чтобы построить дерево зависимостей. Другими словами он вызывает гетеры у реактивных свойств, и запоминает что при вычислении result было вызвано свойство base, а значит при изменении base нужно повторно вычислить result.
                Так зачем это делать каждый раз при обращении к свойству? Ну ладно, один раз в начале построили дерево зависимостей и всё, дальше пользуемся полученными результатами.

                Если упростить, то вся ваша вкладка работает в одном потоке.
                Так я и говорю про UI-поток (в случае с JS он же и единственный). Неужели и правда алгоритм O(n) настолько долго выполняется, что можно успеть невооружённым глазом заметить лаги? И по-прежнему непонятно, откуда взялись долгие обработки, если мы просто добавляем массив в хранилище (внутри commit наверно что-то похожее на state.items = value).

                Или как указали в коментариях выше, можно использовать Object.freeze()
                Мне кажется, лучше тогда вообще не допускать попадания глубоких объектов в data или хранилище, а то это оборачивание костыльно выглядит (для эксперимента, попросите кого-нибудь не в теме попробовать догадаться, что тут происходит и зачем).
                  0
                  Ну ладно, один раз в начале построили дерево зависимостей и всё, дальше пользуемся полученными результатами.

                  Года три назад я, вроде бы, также пользовался такой оптимизацией на долгих вычислениях с большим количеством данных. Но эксперимент показал, что этого делать уже не нужно — console.log в фидле вызывается один раз, даже не смотря на то, что обращение к свойству происходит в цикле.

                  Так я и говорю про UI-поток (в случае с JS он же и единственный). Неужели и правда алгоритм O(n) настолько долго выполняется, что можно успеть невооружённым глазом заметить лаги? И по-прежнему непонятно, откуда взялись долгие обработки, если мы просто добавляем массив в хранилище (внутри commit наверно что-то похожее на state.items = value).

                  Дело не в алгоритме O(n). Тут логика в том, что когда мы присваиваем свойству длинный массив, все элементы которого должны быть отрисованы (а это могут быть большие и сложные объекты (например GeoJSON) с большими и сложными шаблонами), то вью сперва сравнит их по ключу (чтобы не отрисовывать то, что уже отрисовано), а вот все новые элементы синхронно начнёт добавлять в DOM. Если новых элементов массива очень много, то и отрисовка DOM-дерева для них также займёт приличное время (зависит ещё от железа, конечно). Вот пока DOM-дерево создаётся, пока браузер всё это не отрисует — это и есть время лага, вкладка браузера в этот момент просто зафризится.
                  Суть оптимизации в этом и состоит — разбить внесение изменений в DOM-дерево на небольшие кусочки. Да, отрисовка может получиться с рывками, но это всё-равно лучше, чем полностью фризить страницу.
                    0
                    Но эксперимент показал, что этого делать уже не нужно — console.log в фидле вызывается один раз, даже не смотря на то, что обращение к свойству происходит в цикле

                    Вы правы, Vue кэширует значения вычисляемых свойств. И при повторном обращении не вычисляет их заново.


                    НО! Под капотом, он всё равно выполняет некоторую логику, при каждом обращении.


                    Проверить это очень легко: достаточно замерить время выполнения в обоих случаях:


                    https://jsfiddle.net/kozack/3a78q4vj/13/


                    Если посмотреть в консоли, то вы увидите, что staticData вычисляется только один раз, не смотря на многочисленные обращения. И увидите, что optimized вычисляется приблизительно в 10 раз быстрее, благодаря тому, что обращения к staticData сведены к минимуму.

                      0
                      Вы абсолютно правы, спасибо за ваш пример!
                      Я уже после своего комментария понял, что не то проверял)

                      Удивительно то, что скорость вычислений падает аж в 18-20 раз. Значит не зря я несколько лет назад что-то подобное делал.
                      Прям хоть правило для eslint'а пиши, чтобы оно заставляло писать что-то вроде
                      const { anotherComputed } = this;
                      в начале методов и других computed'ов, если обращение к ним просходит в циклах или методах массивов forEach/map/reduce/etc.
              0

              НЛО следует удалить этот случайный комментарий

                0
                Если ваше приложение построено так, что зависит только от объекта верхнего уровня и не ссылается на реактивные данные где-то на несколько уровней ниже

                Не очень понятно, как данные будут изменять в компоненте, если изменились в родителе и вы отключили реактивность?

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое