Оглавление

    Перфоманс

    Версионирование пакетов

    Часто встречается проблема, когда в разных проектах используется один и тот же пакет, но разной версии. Такой пакет будет бандлиться два раза, что приведен к увеличению размера файла. Для избежания такой проблемы в nuxt проектах стоит duplicate-package-checker-webpack-plugin, который подсветит дублирующиеся пакеты.

    /**
     * 💩 Плохо: date-fns будет использоваться два раза, код не переиспользуется
     */
    {
      dependencies: {
        "date-fns": "^2.16.1"
      }
    }
    
    {
      dependencies: {
        "date-fns": "^2.22.1"
      }
    }
    
    /**
     * 😊 Хорошо: общие функции двух проектов объединятся, код переиспользуется
     */
    {
      dependencies: {
        "date-fns": "^2.16.1"
      }
    }
    
    {
      dependencies: {
        "date-fns": "^2.16.1"
      }
    }
    

    Предзагрузка шрифтов

    Шрифтам следует добавлять link[rel="preload"], чтобы запрос за ними отправлялся как можно раньше, а не когда распарсится css с font-face правилом и найдёт использование шрифта. Из-за проблем с CORS не забываем о crossorigin атрибуте.

    <!-- 😊 Хорошо: шрифт загружается в первых запросах -->
    <link 
      rel="preload" 
      href="https://cdn.svyaznoy.ru/upload/web/fonts/SvyaznoySans400.woff2"
      as="font"
      type="font/woff2"
      crossorigin="anonymous"
    >
    

    lodash

    При использовании lodash рекомендуется установить babel-plugin-lodash, чтобы трансформировать импорты функций, иначе вся библиотека окажется в бандле, так как она не сможет три-шейкаться.

    /**
     * Трансформируем такие импорты
     */
    import _ from 'lodash'
    import { add } from 'lodash/fp'
    
    const addOne = add(1)
    _.map([1, 2, 3], addOne)
    
    /**
     * в импорты которые нормально работает с тришейкингом
     */
    import _add from 'lodash/fp/add'
    import _map from 'lodash/map'
    
    const addOne = _add(1)
    _map([1, 2, 3], addOne)
    

    scroll и touch события

    Для таких события следует добавлять passive: true в addEventListener, чтобы не мешать браузеру выполнять свою работу и увеличить перфоманс скроллинга страницы. Во vue-компонентах есть модификатор .passive.

    /**
     * 😊 Хорошо: событие не мешает браузеру работать
     */
    document.addEventListener('touchstart', onTouchStart, { passive: true })
    

    Работа с размерами элемента

    Когда нужно работать с шириной или смещением элементов по многу раз, следует сначала получить все данные об элементах, а потом манипулировать классами или стилями, чтобы не вызывать лишние layout операции.

    /**
     * 💩 Плохо: перерасчёт дорогой операции layout
     */
    const width1 = div1.clientWidth
    div1.style.width = width1 + 'px'
    
    const width2 = div2.clientWidth
    div2.style.width = width2 + 'px'
    
    /**
     * 😊 Хорошо: оптизированные расчёты
     */
    const width1 = div1.clientWidth
    const width2 = div2.clientWidth
    
    div1.style.width = width1 + 'px'
    div2.style.width = width2 + 'px'
    

    Ленивая гидрация

    Существует способ оптимизировать метрику Time to interactive (TTI) в nuxt приложениях за счёт ленивой гидрации с помощью библиотеки vue-lazy-hydration.

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

    При использовании ленивой гидрации в связке со swiper можно воспользоваться классом .swiper-container-initialized. Если такого класса нет – значит слайдер ещё не инициализирован и мы должны с помощью css расположить слайды так, чтобы при инициализации слайдера слайды не дёрнулись. Как только слайдер проинициализуется – селектор перестанет работать, а swiper даст слайдам нужную ширину и станет интерактивным.

    <template>
      <lazy-hydrate when-visible class="lazy-example">
        <swiper class="lazy-example__slider">
          <swiper-slide></swiper-slide>
          <swiper-slide></swiper-slide>
          <swiper-slide></swiper-slide>
        </swiper>
      </lazy-hydrate>
    </template>
    
    <style lang="scss" scoped>
    .lazy-example {
      &__slider {
        height: 100%;
    
        &:not(.swiper-container-initialized) {
          overflow: hidden;
    
          .slider-wrapper {
            display: flex;
            width: 100%;
            height: 100%;
          }
    
          .swiper-slide {
            flex-shrink: 0;
            width: 100%;
          }
        }
      }
    }
    </style>
    

    Мобильные и десктопные компоненты

    Если компонент имеет разную версию на мобильном и десктопном устройстве, то его нужно грузить через динамические импорты, чтобы в бандл не попадало обе версии.

    <template>
      <component :is="view"></component>
    </template>
    
    <script>
    export default {
      name: 'PageHeader',
      
      components: {
        MobileHeader: () => import('./MobileHeader.vue'),
        DesktopHeader: () => import('./DesktopHeader.vue')
      },
      
      computed: {
        view () {
          return this.$device.isMobile ? 'MobileHeader' : 'DesktopHeader'
        }
      }
    }
    </script>
    

    Локальный кэш геттеров

    Когда компонент имеет какое-то вычисляемое свойство на основе второго вычисляемого свойства, то следует сохранять результат вычисления второго вычисляемого свойства в локальную переменную.

    Рассмотрим пример

    Свойство base вызывается в цикле, что приводит к осложнениям. Каждый раз обращаясь к реактивным данным Vue запускает некоторую логику, чтобы определить как и к каким данным вы обращаетесь построить зависимости и так далее. Эти небольшие накладные расходы суммируются если обращений много, как в нашем примере. Чтобы исправить это достаточно просто обратится к base один раз и сохранить значение в локальную переменную.

    <template>
      <div :style="{ opacity: start / 300 }">
        {{ result }}
      </div>
    </template>
    
    <script>
    export default {
      props: {
        start: {
          type: Number,
          required: true,
        }
      },
      
      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>
    

    Функциональные геттеры vuex

    Иногда это упускается из виду в документации. Функциональные геттеры не кэшируются.

    /**
     * 💩 Плохо: геттер пересчитывается каждый раз
     */
    
    /**
     * Vuex
     */
    getters: {
      itemById: (state) => (itemId) => state.items.find(item => item.id === itemId)
    }
    
    /**
     * Какой-то <item :item-id="itemId" /> компонент:
     */
    computed: {
      item () { 
        return this.$store.getters.itemById(this.itemId)
      }
    }
    
    
    /**
     * 😊 Хорошо: вот это построит объект `itemsByIds` при первом обращении и закэширует результат:
     */
    
    /**
     * Vuex
     */
    getters: {
      itemByIds: (state) => state.items.reduce((out, item) => {
        out[item.id] = item
        
        return out
      }, {})
    }
    
    /**
     * Какой-то <item :item-id="itemId" /> компонент:
     */
    computed: {
      item () { 
        return this.$store.getters.itemsByIds[this.itemId]
      }
    }
    

    Глубокие вотчеры

    Рекомендуется использовать watch только при работе с примитивами. Для каждого сложного watcher создается отдельный computed примитивного типа. Например, если требуется отслеживать свойства id в массиве items, можно отслеживать строку.

    Очевидно, чем точнее условие для срабатывания watcher, тем лучше, тем точнее он срабатывает. Объектный watcher – плохо, deep watcher – еще хуже. Использование deep в коде – частый признак неграмотности разработчика. Типа я не понимаю что делает этот код, какими объектами он оперирует, но что-то иногда не срабатывает, навешу-ка я deep – о вроде работает.

    /**
     * 💩 Плохо: следим за массивом глубоко
     */
    
    watch: {
      items: {
        deep: true,
        handler (newValue, oldValue) {
          
        } 
      }
    }
    
    /**
     * 😊 Хорошо: следим за примитивом
     */
    
    computed: {
      itemsTrigger () {
        return JSON.stringify(items.map(item => item.id))
      }
    },
    
    watch: {
      itemsTrigger (newValue, oldValue) {
        /**
         * Здесь не нужен JSON.parse – дешевле пользоваться исходным this.items
         */
      }
    }
    

    Ограничение реактивности через Object.freeze

    Использование Object.freeze не даёт vue навешивать реактивность на данные, что может сократить потребление памяти.

    Зачастую мы запрашиваем какие-либо данные и складываем их в стейт vuex. Данные статичны, однако 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))
        }
      }
    }
    

    Лишний ререндер компонентов

    В случае, если компонент имеет какое-то активное состояние в зависимости от пропса, следует вычислять состояние в родителе, чтобы избежать лишних ререндеров компонента.

    <!-- 💩 Плохо: при изменении activeId 
    ререндерятся все компоненты -->
    <template>
      <list-item
        v-for="item in list"
        :id="item.id"
        :active-id="activeId"
      />
    </template>
    
    <!-- 😊 Хорошо: для большинства компонентов is-active 
    останется неизменным и ререндера не произойдёт  -->
    <template>
      <list-item
        v-for="item in list"
        :id="item.id"
        :is-active="item.id === activeId"
      />
    </template>