Директива v-memo позволяет пропустить обновление поддерева если переданный массив зависимостей не изменился — аналог React.memo:
<!-- Этот элемент обновится только если item.id или selected изменился -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, selected === item.id]">
<p>{{ item.name }}</p>
<p>{{ item.description }}</p>
<!-- ...много тяжёлого контента... -->
</div>v-memo="[]" полностью заморозит элемент — он никогда не будет обновляться.
Обычные ref и reactive делают объект реактивным рекурсивно — каждое вложенное свойство отслеживается. Для больших объектов это дорого:
import { shallowRef, shallowReactive, triggerRef } from 'vue'
// Только верхний уровень реактивен
const bigList = shallowRef([])
// Мутация вложенных данных не триггерит обновления...
bigList.value.push({ id: 1 })
// ...нужно явно сообщить Vue об изменении
triggerRef(bigList)
// shallowReactive: только собственные свойства реактивны
const state = shallowReactive({
count: 0, // реактивно
user: { name: 'Alice' } // НЕ реактивно внутри
})Рендерить 10 000 элементов одновременно — катастрофа для производительности. **Виртуализация** рендерит только видимые элементы:
// С vue-virtual-scroller или @tanstack/virtual
import { useVirtualList } from '@vueuse/core'
const { list, containerProps, wrapperProps } = useVirtualList(items, {
itemHeight: 50, // высота каждого элемента
})<div v-bind="containerProps" style="height: 400px; overflow-y: auto;">
<div v-bind="wrapperProps">
<div v-for="item in list" :key="item.index" style="height: 50px">
{{ item.data.name }}
</div>
</div>
</div>import { defineAsyncComponent } from 'vue'
// Компонент загружается только когда он нужен
const HeavyChart = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
)
// С дополнительными опциями
const AsyncComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner, // пока грузится
errorComponent: ErrorMessage, // при ошибке
delay: 200, // задержка перед показом LoadingSpinner
timeout: 10000, // таймаут загрузки
})Кэширует экземпляры компонентов вместо их уничтожения:
<!-- Вкладки не теряют состояние при переключении -->
<keep-alive :include="['TabA', 'TabB']" :max="5">
<component :is="currentTab" />
</keep-alive>// Хуки в кэшированных компонентах
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// Вызывается при "показе" кэшированного компонента
refreshData()
})
onDeactivated(() => {
// Вызывается при "скрытии" (не unmount!)
pauseVideo()
})// vite.config.js — разбивка на чанки
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor': ['vue', 'vue-router', 'pinia'],
'charts': ['chart.js', 'd3'],
}
}
}
}
})v-memo для тяжёлых списковshallowRef для больших неизменяемых структурdefineAsyncComponent для тяжёлых компонентов<keep-alive> для часто переключаемых вкладокv-once для статичного контентаvite-bundle-visualizerМемоизация и виртуальный список (windowing) — ключевые техники оптимизации, аналог того, что делает Vue внутри
// ============================================
// 1. Мемоизация — кэширование результатов функции
// ============================================
function memoize(fn) {
const cache = new Map()
return function(...args) {
const key = JSON.stringify(args)
if (cache.has(key)) {
console.log(`[memo] cache HIT для ${key}`)
return cache.get(key)
}
console.log(`[memo] cache MISS для ${key} — вычисляем...`)
const result = fn.apply(this, args)
cache.set(key, result)
return result
}
}
// Дорогая функция (симуляция)
let callCount = 0
const expensiveCalc = memoize((n) => {
callCount++
// Симулируем тяжёлые вычисления
let result = 0
for (let i = 0; i < n * 1000; i++) result += Math.sqrt(i)
return Math.round(result)
})
console.log('=== Мемоизация ===')
console.log(expensiveCalc(100)) // вычисляем
console.log(expensiveCalc(100)) // из кэша
console.log(expensiveCalc(200)) // вычисляем
console.log(expensiveCalc(100)) // из кэша
console.log(`Реальных вызовов: ${callCount} (вместо 4)`)
// ============================================
// 2. Виртуальный список (windowing)
// ============================================
class VirtualList {
constructor(itemHeight = 50, containerHeight = 300) {
this.items = []
this.itemHeight = itemHeight
this.containerHeight = containerHeight
this.scrollTop = 0
this.overscan = 3 // рендерим N лишних элементов сверху/снизу
}
setItems(arr) {
this.items = arr
return this
}
// Общая высота всего списка (для скроллбара)
get totalHeight() {
return this.items.length * this.itemHeight
}
// Установить позицию прокрутки
setScrollTop(scrollTop) {
this.scrollTop = Math.max(0, scrollTop)
return this
}
// Получить диапазон видимых элементов
getVisibleRange() {
const startIndex = Math.floor(this.scrollTop / this.itemHeight)
const visibleCount = Math.ceil(this.containerHeight / this.itemHeight)
const endIndex = startIndex + visibleCount
// С overscan — рендерим немного больше для плавности
return {
start: Math.max(0, startIndex - this.overscan),
end: Math.min(this.items.length - 1, endIndex + this.overscan)
}
}
// Получить видимые элементы с их позициями
getVisibleItems() {
const { start, end } = this.getVisibleRange()
const result = []
for (let i = start; i <= end; i++) {
result.push({
index: i,
data: this.items[i],
offsetY: i * this.itemHeight, // позиция для CSS transform
})
}
return result
}
// Статистика рендера
stats() {
const visible = this.getVisibleItems()
return {
total: this.items.length,
rendered: visible.length,
savings: `${((1 - visible.length / this.items.length) * 100).toFixed(1)}%`,
range: this.getVisibleRange()
}
}
}
// --- Демонстрация виртуального списка ---
console.log('\n=== Виртуальный список ===')
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Элемент #${i}`,
value: Math.random().toFixed(2)
}))
const vList = new VirtualList(50, 300)
vList.setItems(items)
// Прокрутка в начало
vList.setScrollTop(0)
let stats = vList.stats()
console.log('Scroll: 0px', stats)
console.log('Видимые элементы:', vList.getVisibleItems().slice(0, 3).map(i => i.data.name))
// Прокрутка в середину
vList.setScrollTop(25000)
stats = vList.stats()
console.log('\nScroll: 25000px', stats)
console.log('Видимые элементы:', vList.getVisibleItems().slice(0, 3).map(i => i.data.name))
// Прокрутка в конец
vList.setScrollTop(vList.totalHeight)
stats = vList.stats()
console.log('\nScroll: end', stats)Директива v-memo позволяет пропустить обновление поддерева если переданный массив зависимостей не изменился — аналог React.memo:
<!-- Этот элемент обновится только если item.id или selected изменился -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, selected === item.id]">
<p>{{ item.name }}</p>
<p>{{ item.description }}</p>
<!-- ...много тяжёлого контента... -->
</div>v-memo="[]" полностью заморозит элемент — он никогда не будет обновляться.
Обычные ref и reactive делают объект реактивным рекурсивно — каждое вложенное свойство отслеживается. Для больших объектов это дорого:
import { shallowRef, shallowReactive, triggerRef } from 'vue'
// Только верхний уровень реактивен
const bigList = shallowRef([])
// Мутация вложенных данных не триггерит обновления...
bigList.value.push({ id: 1 })
// ...нужно явно сообщить Vue об изменении
triggerRef(bigList)
// shallowReactive: только собственные свойства реактивны
const state = shallowReactive({
count: 0, // реактивно
user: { name: 'Alice' } // НЕ реактивно внутри
})Рендерить 10 000 элементов одновременно — катастрофа для производительности. **Виртуализация** рендерит только видимые элементы:
// С vue-virtual-scroller или @tanstack/virtual
import { useVirtualList } from '@vueuse/core'
const { list, containerProps, wrapperProps } = useVirtualList(items, {
itemHeight: 50, // высота каждого элемента
})<div v-bind="containerProps" style="height: 400px; overflow-y: auto;">
<div v-bind="wrapperProps">
<div v-for="item in list" :key="item.index" style="height: 50px">
{{ item.data.name }}
</div>
</div>
</div>import { defineAsyncComponent } from 'vue'
// Компонент загружается только когда он нужен
const HeavyChart = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
)
// С дополнительными опциями
const AsyncComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner, // пока грузится
errorComponent: ErrorMessage, // при ошибке
delay: 200, // задержка перед показом LoadingSpinner
timeout: 10000, // таймаут загрузки
})Кэширует экземпляры компонентов вместо их уничтожения:
<!-- Вкладки не теряют состояние при переключении -->
<keep-alive :include="['TabA', 'TabB']" :max="5">
<component :is="currentTab" />
</keep-alive>// Хуки в кэшированных компонентах
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// Вызывается при "показе" кэшированного компонента
refreshData()
})
onDeactivated(() => {
// Вызывается при "скрытии" (не unmount!)
pauseVideo()
})// vite.config.js — разбивка на чанки
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor': ['vue', 'vue-router', 'pinia'],
'charts': ['chart.js', 'd3'],
}
}
}
}
})v-memo для тяжёлых списковshallowRef для больших неизменяемых структурdefineAsyncComponent для тяжёлых компонентов<keep-alive> для часто переключаемых вкладокv-once для статичного контентаvite-bundle-visualizerМемоизация и виртуальный список (windowing) — ключевые техники оптимизации, аналог того, что делает Vue внутри
// ============================================
// 1. Мемоизация — кэширование результатов функции
// ============================================
function memoize(fn) {
const cache = new Map()
return function(...args) {
const key = JSON.stringify(args)
if (cache.has(key)) {
console.log(`[memo] cache HIT для ${key}`)
return cache.get(key)
}
console.log(`[memo] cache MISS для ${key} — вычисляем...`)
const result = fn.apply(this, args)
cache.set(key, result)
return result
}
}
// Дорогая функция (симуляция)
let callCount = 0
const expensiveCalc = memoize((n) => {
callCount++
// Симулируем тяжёлые вычисления
let result = 0
for (let i = 0; i < n * 1000; i++) result += Math.sqrt(i)
return Math.round(result)
})
console.log('=== Мемоизация ===')
console.log(expensiveCalc(100)) // вычисляем
console.log(expensiveCalc(100)) // из кэша
console.log(expensiveCalc(200)) // вычисляем
console.log(expensiveCalc(100)) // из кэша
console.log(`Реальных вызовов: ${callCount} (вместо 4)`)
// ============================================
// 2. Виртуальный список (windowing)
// ============================================
class VirtualList {
constructor(itemHeight = 50, containerHeight = 300) {
this.items = []
this.itemHeight = itemHeight
this.containerHeight = containerHeight
this.scrollTop = 0
this.overscan = 3 // рендерим N лишних элементов сверху/снизу
}
setItems(arr) {
this.items = arr
return this
}
// Общая высота всего списка (для скроллбара)
get totalHeight() {
return this.items.length * this.itemHeight
}
// Установить позицию прокрутки
setScrollTop(scrollTop) {
this.scrollTop = Math.max(0, scrollTop)
return this
}
// Получить диапазон видимых элементов
getVisibleRange() {
const startIndex = Math.floor(this.scrollTop / this.itemHeight)
const visibleCount = Math.ceil(this.containerHeight / this.itemHeight)
const endIndex = startIndex + visibleCount
// С overscan — рендерим немного больше для плавности
return {
start: Math.max(0, startIndex - this.overscan),
end: Math.min(this.items.length - 1, endIndex + this.overscan)
}
}
// Получить видимые элементы с их позициями
getVisibleItems() {
const { start, end } = this.getVisibleRange()
const result = []
for (let i = start; i <= end; i++) {
result.push({
index: i,
data: this.items[i],
offsetY: i * this.itemHeight, // позиция для CSS transform
})
}
return result
}
// Статистика рендера
stats() {
const visible = this.getVisibleItems()
return {
total: this.items.length,
rendered: visible.length,
savings: `${((1 - visible.length / this.items.length) * 100).toFixed(1)}%`,
range: this.getVisibleRange()
}
}
}
// --- Демонстрация виртуального списка ---
console.log('\n=== Виртуальный список ===')
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Элемент #${i}`,
value: Math.random().toFixed(2)
}))
const vList = new VirtualList(50, 300)
vList.setItems(items)
// Прокрутка в начало
vList.setScrollTop(0)
let stats = vList.stats()
console.log('Scroll: 0px', stats)
console.log('Видимые элементы:', vList.getVisibleItems().slice(0, 3).map(i => i.data.name))
// Прокрутка в середину
vList.setScrollTop(25000)
stats = vList.stats()
console.log('\nScroll: 25000px', stats)
console.log('Видимые элементы:', vList.getVisibleItems().slice(0, 3).map(i => i.data.name))
// Прокрутка в конец
vList.setScrollTop(vList.totalHeight)
stats = vList.stats()
console.log('\nScroll: end', stats)Реализуй функцию `memoizeDeep(fn)` — мемоизацию с глубоким сравнением аргументов через `JSON.stringify`. И класс `VirtualList` с методами: `setItems(arr)` — установить все элементы, `setViewport(start, end)` — установить видимый диапазон по индексам, `getVisibleItems()` — вернуть только элементы в viewport (от start до end включительно).
В memoizeDeep ключ = JSON.stringify(args), это позволяет глубоко сравнивать объекты и массивы. В VirtualList.getVisibleItems() используй this.items.slice(this.viewportStart, this.viewportEnd + 1) — slice принимает конечный индекс не включительно, поэтому +1.
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке