Filter + indexOf vs Set: как быстро нормализовать товары из API
-
Когда с API летит список товаров с дублями по ID, нужно их убрать — и желательно без лагов на фронте. Классический подход
filter+indexOfработает, но сожрёт производительность на больших объёмах. Set выглядит проще, но с объектами по ссылкам он бесполезен. Разбираемся, почему один способ быстрее другого в 10 раз, и как не наступить на грабли.Почему filter + indexOf — это костыль на больших данных
Все начинается просто: нужно отфильтровать массив и оставить только первое вхождение каждого элемента. Код выглядит честно и понятно:
const filtered = products.filter((item, index) => products.indexOf(item.id) === index );Смотрится красиво, но под капотом происходит O(n²) ужас. Для каждого элемента в цикле filter мы снова прошиваем весь массив через indexOf, ища его первое вхождение. На 15 тысячах товаров это 225 миллионов операций сравнения. Плюс indexOf работает с ссылками, поэтому два объекта с одинаковым ID будут считаться разными — нужна дополнительная логика через JSON.stringify, что ещё больше замедляет.
Результат: фильтр по категории в админке лагает, юзеры видят висячий UI, всем грустно. Когда allowedIds из стора — это 3 тысячи штук, а основной список 15 тысяч, каждое изменение фильтра превращается в пытку.
Проблема в том, что indexOf вызывается для каждого элемента, и каждый раз он ползёт по всему массиву заново. Никакой оптимизации, никакой памяти о том, что ты уже ищешь. Просто тупо O(n) × O(n) = боль.
Set: волшебство с примитивами, разочарование с объектами
Set — это структура данных, которая хранит только уникальные значения и даёт доступ за O(1). Звучит как решение всех проблем:
const uniqueIds = new Set(bigArray.map(id => id)); const filtered = products.filter(p => uniqueIds.has(p.id));Это O(n) вместо O(n²), и разница ощущается сразу — мгновенно вместо лагов. С примитивами (числа, строки) Set работает идеально: добавляешь элемент, Set сам проверяет, нет ли его уже, и хранит только уникальные.
Но есть нюанс, который многие пропускают. Set проверяет равенство по значению для примитивов, но для объектов — по ссылке. Два объекта с одинаковым ID будут считаться разными, потому что это разные экземпляры в памяти. Поэтому просто так set(objects) не сработает — нужно класть в Set ID, а не сами объекты.
Ещё один момент: если хранить строки длинных ID (например, UUID), могут быть хэш-коллизии при нехватке памяти, но это редкая проблема в реальных приложениях. Главное — Set безопасен и быстр, если использовать его правильно.
Правильный паттерн: Set для индексирования, Map для объектов
Для нормализации товаров из API нужен гибридный подход. Если нужно отфильтровать объекты по ID, используй Set только для ID:
const allowedIds = new Set(categories.map(c => c.id)); const filtered = products.filter(p => allowedIds.has(p.id));Это читаемо, быстро, и нет магии. Фильтр остаётся простым, все условия явные.
Если же нужно полностью нормализовать список и убрать дублирующиеся объекты, Map по ID — золотая середина:
const normalized = new Map(products.map(p => [p.id, p])); const result = Array.from(normalized.values());Мап сам отсекает дубли при set: если два объекта с одинаковым ID, второй перезапишет первый. О(n) на создание, O(1) на доступ. Если нужна логика типа «оставить последний по дате» или «выбрать по какому-то критерию», можно добавить условие в момент set:
products.forEach(p => { const existing = normalized.get(p.id); if (!existing || p.updatedAt > existing.updatedAt) { normalized.set(p.id, p); } });Сравнение на реальных данных
Подход Сложность Скорость на 15k товаров С объектами Читаемость filter + indexOf O(n²) Лаги, ~500ms+ Требует JSON.stringify Средняя Set (только ID) O(n) Мгновенно, ~5ms Работает, но нужен отдельный фильтр Высокая Map по ID O(n) Мгновенно, ~8ms Работает идеально, дубли отсекаются Высокая filter + Set O(n) Мгновенно, ~3ms Работает, но хак Хорошая Видишь разницу? С Set и Map мы уходим с O(n²) на O(n) — это не просто ускорение, это спасение UX. На 15 тысячах товарах — ускорение в 10 раз и больше.
На практике: пример из админки каталога
Типичная задача: есть таблица товаров, нужно отфильтровать по категории и статусу наличия. allowedIds из фильтра — 3000 товаров, основной список — 15000. Юзер меняет фильтр, данные должны обновиться без задержки.
Старый способ (костыль):
const filtered = allProducts.filter((item, index) => allowedIds.indexOf(item.id) === index );Ждём, пока indexOf переберёт массив allowedIds для каждого товара. На каждое изменение фильтра — ~500ms зависания. Юзер кликает, видит freezing, раздражается.
Новый способ (быстро):
const allowedIdSet = new Set(allowedIds); const filtered = allProducts.filter(p => allowedIdSet.has(p.id));Есть дубли в списке товаров? Дополняем Map:
const normalized = new Map(); allProducts.forEach(p => { if (allowedIdSet.has(p.id)) { const existing = normalized.get(p.id); if (!existing || p.stock > existing.stock) { normalized.set(p.id, p); // берём вариант с большим stock } } }); const filtered = Array.from(normalized.values());Результат: мгновенно, UI не зависает, юзер доволен. На этом примере ускорение ощущается физически.
Когда filter + indexOf ещё используется
Есть кейсы, когда filter + indexOf остаётся единственным разумным вариантом — например, если нужно сохранить порядок первого появления элемента и данные постоянно меняются. Set гарантирует уникальность, но не гарантирует порядок в старых браузерах (хотя в современных порядок вставки соблюдается).
Ещё встречается старый легаси-код, который почему-то переписывать не хотят — работает, значит работает. Но если ты пишешь новый код и видишь filter + indexOf на больших массивах — это red flag. Это признак либо забывчивости, либо незнания особенностей производительности.
Проблема в том, что на маленьких массивах (50-100 элементов) разница не видна, поэтому никто не замечает, пока не упрёшься в реальные данные. А потом начинаются поиски баг-репортов и оптимизации, которые можно было избежать с самого начала.
Финальный ударный тест
Итак, рецепт для нормализации товаров из API:
- Если фильтруешь по ID — Set для индексирования, filter остаётся честным:
new Set(allowedIds)+has()вместоindexOf(). - Если удаляешь дубли полностью — Map по ID:
new Map(products.map(p => [p.id, p]))+Array.from()в конце. - Если нужна сложная логика (выбор по критерию) — forEach с условием в момент set, дешевле, чем дополнительные циклы.
Экономия на производительности — это не просто быстрее, это улучшение опыта пользователя. Мгновенный отклик на клик, отсутствие зависаний, гладкий UI. На фронте это заметно сразу, на бэке тоже, но там интеллектуальнее относятся к сложности алгоритмов.
Одно последнее: профилируй на реальных данных. Может быть, у тебя массивы намного меньше, и разницы не будет вообще. Может быть, наоборот — 100 тысяч товаров, и Set сэкономит тебе секунды. Инструменты в браузере (Performance, DevTools) покажут истину быстрее, чем любые статьи. Но основной закон остаётся: O(n) лучше, чем O(n²), всегда и везде.
- Если фильтруешь по ID — Set для индексирования, filter остаётся честным:
Здравствуйте! Похоже, вас заинтересовала эта беседа, но у вас ещё нет аккаунта.
Надоело каждый раз пролистывать одни и те же посты? Зарегистрировав аккаунт, вы всегда будете возвращаться на ту же страницу, где были раньше, и сможете выбирать, получать ли уведомления о новых ответах (по электронной почте или в виде push-уведомлений). Вы также сможете сохранять закладки и ставить лайки постам, чтобы выразить свою благодарность другим участникам сообщества.
С вашими комментариями этот пост мог бы стать ещё лучше 💗
Зарегистрироваться Войти© 2024 - 2026 ExLends, Inc. Все права защищены.