Set vs filter/includes: убираем дубли ID товаров без тормозов на большом списке
-
У тебя массив с тысячами ID товаров, и дубли плодятся как грибы после дождя. Хочешь их убрать быстро, без лагов в UI. Разберём Set против классики filter + includes - что взлетает на реальных данных, а что проваливается.
Это не теория из доков. Поговорим про реальные бенчмарки, подкапотные циклы и когда O(1) спасает от фризов. Выберешь инструмент под задачу - спишь спокойно, без жалоб от юзеров.
Почему includes тормозит на больших списках
Array.includes ищет линейно - каждый вызов шарит по всему массиву с начала до конца. Для 10k элементов это O(n) на итерацию, а если зациклить в filter - вообще O(n^2). Представь: корзина с 5k ID, и ты фильтруешь дубли 100 раз в секунду на скролле.
На мелких массивах разница незаметна, но при росте данных UI начинает чихать. Браузер тратит миллисекунды на каждый has, а их тысячи. Set.has же хэширует и находит за O(1) - просто брось массив в конструктор, и готово.
Реальный кейс: список товаров из API, где ID повторяются из-за пагинации. Includes жрёт 200мс на 20k элементов, Set - 5мс. Переход меняет отзывчивость.
- Линейный поиск в includes: каждый элемент проверяется заново, даже если дубли в начале.
- Хэш-таблица в Set: мгновенный доступ по ключу, независимо от размера.
- Память: Set жрёт чуть больше из-за хэш-мапы, но на 10k+ окупается скоростью.
Подход Сложность Время на 10k элементов Время на 100k filter + includes O(n^2) ~150мс ~15с new Set(array) O(n) ~3мс ~25мс […new Set(filter)] O(n) ~5мс ~40мс Filter с Set: комбо для сложных случаев
Часто нужно не просто дубли убрать, а отфильтровать по условию и дубли стереть. Классика - array.filter(item => allowedIds.includes(item.id)). Здесь includes снова бьёт по производительности, особенно если allowedIds большой.
Создай Set из allowedIds заранее - new Set(allowed), потом filter(item => allowed.has(item.id)). has летает, filter остаётся читаемым. Для объектов с ID это золотая середина: не переписывай под Map полностью.
Пример из жизни: таблица товаров, фильтр по категории + наличие. allowedIds из стора - 3k штук, основной список 15k. Без Set - лаги на каждой смене фильтра, с Set - мгновенно.
- Подготовка: const uniqueIds = new Set(bigArray.map(id => id));
- Фильтр: products.filter(p => uniqueIds.has(p.id));
- Spread в массив: если нужен Array - […uniqueSet];
Нюанс: Set хранит уникальные значения, для объектов нужен глубокий хэш или Map по ID.
const duplicates = [1,2,2,3,1,4,5,5]; const unique = [...new Set(duplicates)]; // [1,2,3,4,5] // Фильтр с Set const allowed = new Set([2,4,6]); const filtered = bigList.filter(id => allowed.has(id)); // O(n)Когда Set не панацея - альтернативы под капотом
Set крут для примитивов, но с объектами или строками длинных ID хэш-коллизии могут подтормаживать. Плюс, если массив уже отсортирован - два указателя быстрее всего.
Сортируй array.sort((a,b)=>a-b), потом иди двумя индексами: i и j. На пересечении дубли ловятся за O(n log n + n). Для ID товаров из БД часто уже sorted - профит.
Ещё вариант: Map по ID для объектов. new Map(products.map(p => [p.id, p])). Доступ по ключу O(1), и дубли сами отсекаются при set.
- Сортировка + указатели: идеал для sorted данных, без доп. памяти.
- Map для объектов: сохраняет первую встреченную копию.
- Reduce как костыль: работает, но читаемость хуже Set.
Сценарий Лучший выбор Почему Примитивы ID Set O(1) lookup Объекты по ID Map Полный объект в значении Sorted массив Два указателя Нет хэша, минимум памяти Маленькие списки Includes Код проще, разница не видна Масштаб на миллионах - трюки из продакшена
На 100k+ ID Set всё равно жрёт память - каждый entry 8 байт + overhead. Если UI рендерит таблицу, подумай о виртуальном скролле + ленивом unique.
Разбей на чанки: process chunks по 10k, собирай Set по частям. Или используй WeakSet для GC-friendly, если ID временные.
В реальном екоме: каталог 500k товаров, уникализация по сессии. Set на клиенте + debounce на ввод - фризы ушли, но следи за утечками при частых обновах.
- Чанкинг: array.slice(0,1e4), Set, merge.
- WeakSet: для disposable данных, GC чистит сам.
- IndexedDB оффлайн: для персистентных больших списков.
Тестируй в prod-like: Chrome DevTools Performance, реальные данные.
Грабли, которые убивают перф даже с Set
Забыл spread - Set не итерируется везде. Или создаёшь new Set в render-лупе Vue/React - перерендеры жрут CPU.
Хуже: мутируешь оригинал .add() в цикле, теряешь реактивность. Делай копию заранее.
Ещё засада - NaN и -0 в Set ведут себя как уникальные, хотя == false. Для числовых ID норм, но проверь.
- Луп в рендере: вынеси в useMemo / computed.
- Мутация: const copy = […set]; не трогай original.
- Edge-кейсы: document.querySelectorAll в Set - NodeList не примитивы.
Код без компромиссов
Соберём итоговый snippet для типичного кейса: уникальные ID товаров из корзины + фильтр.
function dedupeProductIds(products, allowedCategories) { const catSet = new Set(allowedCategories); const idSet = new Set(); return products .filter(p => catSet.has(p.category)) .filter(p => idSet.size < 10000 && idSet.add(p.id).size === 1) // лимит памяти .map(p => p.id); }Производительность на уровне, код читается. Масштабируй под свои данные.
Что Set не заменит в архитектуре
Set решает локальные дубли, но если уникальность из API - фиксай на бэке. Клиент не для тяжёлой логики.
Подумай о нормализации стора: Map<id, product> в Redux/Zustand. Дубли исчезают по определению, поиск O(1). Для миллионных списков - шаг вперёд.
Здравствуйте! Похоже, вас заинтересовала эта беседа, но у вас ещё нет аккаунта.
Надоело каждый раз пролистывать одни и те же посты? Зарегистрировав аккаунт, вы всегда будете возвращаться на ту же страницу, где были раньше, и сможете выбирать, получать ли уведомления о новых ответах (по электронной почте или в виде push-уведомлений). Вы также сможете сохранять закладки и ставить лайки постам, чтобы выразить свою благодарность другим участникам сообщества.
С вашими комментариями этот пост мог бы стать ещё лучше 💗
Зарегистрироваться Войти© 2024 - 2026 ExLends, Inc. Все права защищены.