Оглавление
Перфоманс
Версионирование пакетов
Часто встречается проблема, когда в разных проектах используется один и тот же пакет, но разной версии. Такой пакет будет бандлиться два раза, что приведен к увеличению размера файла. Для избежания такой проблемы в 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>