Компонент Select предназначен для отображения и выбора
значения из списка опций. В реальных приложениях список может содержать
сотни или тысячи элементов: пользователи, товары, категории, города,
идентификаторы записей и т.п. При прямом рендеринге большого массива
данных возникают проблемы:
Виртуальный скроллинг решает эту проблему, отображая только те элементы списка, которые находятся в видимой области, а остальные элементы подгружаются динамически при прокрутке.
Библиотека Naive UI реализует виртуализацию в компоненте
NSelect, что позволяет эффективно работать со списками
размером в десятки тысяч элементов.
Обычный список:
10000 элементов -> 10000 DOM-узлов
Виртуализированный список:
10000 элементов -> ~20–40 DOM-узлов
Виртуализация строится на следующих принципах:
1. Вычисление высоты контейнера
Контейнер списка имеет фиксированную высоту.
containerHeight = visibleItems * itemHeight
2. Определение видимого диапазона
На основе текущего положения прокрутки определяется диапазон элементов:
startIndex
endIndex
3. Отрисовка только видимых элементов
DOM содержит только элементы из этого диапазона.
4. Использование пустого пространства
Для имитации полной высоты списка используется внутренний контейнер с padding или transform.
totalHeight = itemsCount * itemHeight
Таким образом создаётся ощущение полного списка.
В Naive UI виртуальный скроллинг уже встроен в
NSelect и активируется автоматически при использовании
большого массива опций.
Простейший пример:
import { NSelect } from "naive-ui"
import { ref } from "vue"
const options = Array.from({ length: 10000 }).map((_, i) => ({
label: `Option ${i}`,
value: i
}))
const value = ref(null)
<n-select
v-model:value="value"
:options="options"
filterable
/>
Даже при 10 000 элементов компонент остаётся отзывчивым благодаря виртуализации.
Ключевым параметром виртуального скроллинга является высота списка. Она определяет количество элементов, которые одновременно находятся в DOM.
Свойство:
menu-props
Пример настройки:
<n-select
v-model:value="value"
:options="options"
:menu-props="{ style: { maxHeight: '300px' } }"
/>
Если высота контейнера составляет 300px, а высота
элемента около 34px, одновременно будет отображаться
примерно:
300 / 34 ≈ 8–9 элементов
Это минимизирует количество DOM-узлов.
Для корректной виртуализации библиотеке необходимо знать высоту одного элемента списка.
В Naive UI используется приблизительное значение,
рассчитанное для стандартных опций.
Если используется кастомный рендер, высота может измениться. В таких случаях требуется обеспечить одинаковую высоту элементов.
Пример кастомного рендера:
<n-select
v-model:value="value"
:options="options"
:render-option="renderOption"
/>
function renderOption({ node, option }) {
return h(
"div",
{
style: {
padding: "8px 12px",
display: "flex",
alignItems: "center"
}
},
option.label
)
}
При использовании нестандартной высоты важно сохранять фиксированную высоту строки, иначе вычисления виртуального списка могут нарушиться.
NSelect поддерживает фильтрацию
(filterable). При вводе текста происходит:
Пример:
<n-select
v-model:value="value"
:options="options"
filterable
clearable
/>
Алгоритм фильтрации:
input -> filter(options) -> newOptions
После фильтрации:
totalHeight = newOptions.length * itemHeight
Виртуальный список автоматически адаптируется.
При очень больших наборах данных часто используется ленивая загрузка.
Типичный сценарий:
Пример:
const options = ref([])
const loading = ref(false)
async function search(value) {
loading.value = true
const response = await fetch(`/api/users?q=${value}`)
const data = await response.json()
options.value = data.map(user => ({
label: user.name,
value: user.id
}))
loading.value = false
}
<n-select
v-model:value="value"
:options="options"
filterable
remote
:loading="loading"
:on-search="search"
/>
При этом виртуальный скроллинг продолжает работать с текущим набором данных.
Опции желательно создавать один раз.
Плохой вариант:
const options = computed(() =>
items.value.map(i => ({
label: i.name,
value: i.id
}))
)
Лучше:
const options = ref([])
watch(items, () => {
options.value = items.value.map(i => ({
label: i.name,
value: i.id
}))
})
Это предотвращает лишние перерасчёты.
Каждая опция должна иметь стабильный value, который используется как ключ.
value -> key
Плохой пример:
value = index
Хороший пример:
value = database_id
Даже при виртуализации массив из 100 000 объектов может занимать значительную память.
Практика:
5 000 – комфортный диапазон
10 000 – допустимый
> 50 000 – желательно серверная фильтрация
Компонент поддерживает функцию render-option.
import { h } from "vue"
function renderOption({ option }) {
return h(
"div",
{
style: {
display: "flex",
justifyContent: "space-between"
}
},
[
option.label,
h("span", { style: { opacity: 0.5 } }, option.value)
]
)
}
<n-select
:options="options"
:render-option="renderOption"
/>
Виртуализация остаётся активной, поскольку компонент управляет контейнером списка.
Naive UI поддерживает группировку:
const options = [
{
type: "group",
label: "Frontend",
key: "frontend",
children: [
{ label: "Vue", value: "vue" },
{ label: "React", value: "react" }
]
},
{
type: "group",
label: "Backend",
key: "backend",
children: [
{ label: "Node.js", value: "node" },
{ label: "Go", value: "go" }
]
}
]
<n-select :options="options" />
При виртуализации группы учитываются как отдельные элементы списка.
Если элементы имеют различную высоту:
item1 = 32px
item2 = 70px
item3 = 40px
виртуальный список может неправильно вычислять позицию.
Рекомендуется:
Если внутри опций используются:
это может снижать производительность.
Лучше использовать:
При полной замене массива:
options.value = newArray
виртуальный список пересчитывает структуру.
Если изменения частичные, эффективнее обновлять элементы внутри массива.
Пример с 20 000 элементов.
const options = Array.from({ length: 20000 }).map((_, i) => ({
label: `User ${i}`,
value: i
}))
<n-select
v-model:value="value"
:options="options"
filterable
placeholder="Select user"
/>
Без виртуализации:
20000 DOM элементов
С виртуализацией:
~30 DOM элементов
Разница в производительности может достигать десятков раз.
Виртуальный скроллинг корректно работает совместно с:
multipletagfilterableremoterender-optionrender-labelПример множественного выбора:
<n-select
v-model:value="value"
:options="options"
multiple
filterable
/>
Даже при выборе множества значений список остаётся виртуализированным.
Компонент NSelect использует внутренний виртуальный
список, реализованный на основе принципов:
scrollTop
itemHeight
visibleCount
bufferSize
Алгоритм:
startIndex = floor(scrollTop / itemHeight)
endIndex = startIndex + visibleCount + buffer
Буфер необходим для предотвращения “пустых” областей при быстрой прокрутке.
При быстром перемещении:
Чтобы избежать скачков интерфейса:
Это позволяет сохранять плавность прокрутки.
Использовать виртуализацию при:
Избегать:
Оптимальная архитектура:
API
↓
store / composable
↓
options[]
↓
NSelect (virtual list)
Такой подход обеспечивает масштабируемость и высокую производительность интерфейса при работе с большими наборами данных.