From 7067397d7b25c0ea2732bea750846b3534d95485 Mon Sep 17 00:00:00 2001 From: Jeremy Thomerson Date: Tue, 24 Mar 2026 17:05:54 -0400 Subject: [PATCH] perf: use Set for O(1) lookups in uniq (#129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both `standardUniq` and `iterateeUniq` used `indexOf` to check for duplicates, which is O(n) per element — making the overall algorithm O(n²). At 40k elements this takes over a second. `Set.has` uses SameValueZero equality, which matches `indexOf` behavior for all practical values (the only difference is NaN, which is now treated as equal to itself — arguably a bug fix). This introduces a runtime dependency on `Set` (ES2015+). The compiled output emits `new Set()` directly with no polyfill, since the shared tsconfig already targets `es2019` — meaning `Set` was already assumed available by every other feature in the compiled output (arrow functions, `const`, etc.). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/utils/uniq.ts | 12 ++++------- tests/utils/uniq.test.ts | 43 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/utils/uniq.ts b/src/utils/uniq.ts index d22f9eb..85a3517 100644 --- a/src/utils/uniq.ts +++ b/src/utils/uniq.ts @@ -1,13 +1,13 @@ const iterateeUniq = (arr: T[], iteratee: (value: T, i: number, arr: T[]) => U): T[] => { const result: T[] = [], - seen: U[] = []; + seen = new Set(); for (let i = 0, length = arr.length; i < length; i++) { const value = arr[i], computed = iteratee(value, i, arr); - if (seen.indexOf(computed) === -1) { - seen.push(computed); + if (!seen.has(computed)) { + seen.add(computed); result.push(value); } } @@ -33,11 +33,7 @@ const sortedUniq = (arr: T[]): T[] => { const standardUniq = (arr: T[]): T[] => { - const result = arr.filter((value, index, _arr) => { - return _arr.indexOf(value) === index; - }); - - return result; + return Array.from(new Set(arr)); }; diff --git a/tests/utils/uniq.test.ts b/tests/utils/uniq.test.ts index c4df57b..28b59d2 100644 --- a/tests/utils/uniq.test.ts +++ b/tests/utils/uniq.test.ts @@ -21,4 +21,47 @@ describe('uniq', () => { expect(uniq([ 4, 4, 1, 1, 2, 3, 4 ], false, isEven)).to.eql([ 4, 1 ]); }); + + it('handles 40,000 elements in under 50ms', () => { + const arr: string[] = []; + + for (let i = 0; i < 40000; i++) { + arr.push('item-' + (Math.random() < 0.3 ? i % Math.floor(40000 * 0.7) : i)); + } + + // Warmup to avoid JIT noise + uniq(arr); + + const start = performance.now(), + result = uniq(arr); + + expect(performance.now() - start).to.be.lessThan(50); + expect(result.length).to.be.greaterThan(0); + expect(result.length).to.be.lessThan(arr.length); + }); + + + it('handles 40,000 elements with iteratee in under 50ms', () => { + const arr: string[] = []; + + for (let i = 0; i < 40000; i++) { + arr.push('item-' + i); + } + + // Iteratee that produces many unique computed values, causing the + // seen array to grow large and indexOf to become O(n) + const iteratee = (v: string): string => { + return v + '-computed'; + }; + + // Warmup + uniq(arr, false, iteratee); + + const start = performance.now(), + result = uniq(arr, false, iteratee); + + expect(performance.now() - start).to.be.lessThan(50); + expect(result.length).to.strictlyEqual(40000); + }); + });