Every time you chain .filter() and .map() on an array, JavaScript creates two new arrays. For small lists this is invisible. For a large dataset, a generator producing values on demand, or a stream where you only need the first handful of results, you've materialized thousands of elements you immediately discard.
Iterator helpers are the lazy alternative. They shipped natively in Chrome 122, Firefox 131, and Safari 18.2 — no library, no polyfill, no build step.
What "lazy" means in practice
Array methods are eager: they run immediately and return a new fully-populated array.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const result = numbers
.filter(n => n % 2 === 0) // → allocates [2, 4, 6, 8, 10]
.map(n => n * n) // → allocates [4, 16, 36, 64, 100]
.slice(0, 3); // → allocates [4, 16, 36]
Enter fullscreen mode Exit fullscreen mode
Three arrays get created and discarded. Only the last one survives.
Iterator helpers are lazy: they build a pipeline description. No element is processed until you ask for one.
const result = Iterator.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
.filter(n => n % 2 === 0)
.map(n => n * n)
.take(3)
.toArray();
// → [4, 16, 36]
Enter fullscreen mode Exit fullscreen mode
Same output. No intermediate arrays. The pipeline processes elements one at a time: 1 hits .filter(), fails, and is discarded. 2 passes .filter(), goes through .map(), counts toward .take(3). The loop stops after three passing elements — the remaining numbers are never visited.
Iterator.from() and what you get back
Iterator.from() wraps any iterable — arrays, Sets, Maps, strings, generator return values — in an object with the iterator helper methods attached.
const set = new Set([1, 1, 2, 2, 3, 3, 4, 5]);
const result = Iterator.from(set)
.filter(n => n % 2 !== 0)
.toArray();
// → [1, 3, 5]
Enter fullscreen mode Exit fullscreen mode
Generators are a natural fit. A generator already produces values on demand; iterator helpers let you build pipelines over them without ever creating an intermediate array.
function* naturals() {
let n = 0;
while (true) yield n++;
}
const firstFiveSquaredEvens = Iterator.from(naturals())
.filter(n => n % 2 === 0)
.map(n => n * n)
.take(5)
.toArray();
// → [0, 4, 16, 36, 64]
Enter fullscreen mode Exit fullscreen mode
You cannot do this with array methods on an infinite generator — .filter() on an array needs to reach the end before it can return anything.
The full method set
Iterator helpers ship ten methods. The first group builds the pipeline (lazy); the second group runs it (terminal):
Lazy: .map(fn) .filter(fn) .take(n) .drop(n) .flatMap(fn)
Terminal: .toArray() .reduce(fn, init) .find(fn) .some(fn) .every(fn) .forEach(fn)
Enter fullscreen mode Exit fullscreen mode
Calling a lazy method returns a new iterator — nothing runs yet. Calling a terminal method pulls values through the entire chain until it has what it needs, then stops.
// .find() stops at the first match; the rest of the data is never touched
const firstLongWord = Iterator.from(wordList)
.filter(w => w.startsWith('pre'))
.find(w => w.length > 10);
Enter fullscreen mode Exit fullscreen mode
A practical example: processing a large list
You're rendering a filterable list of 10,000 records. The user has typed a search term and picked a category. The view shows the first 50 matches.
With arrays:
const visible = allRecords
.filter(r => r.category === selectedCategory) // walks all 10,000
.filter(r => r.name.includes(query)) // walks the filtered set
.slice(0, 50); // finally limits
Enter fullscreen mode Exit fullscreen mode
With iterator helpers:
const visible = Iterator.from(allRecords)
.filter(r => r.category === selectedCategory)
.filter(r => r.name.includes(query))
.take(50)
.toArray();
Enter fullscreen mode Exit fullscreen mode
The iterator version stops as soon as it has 50 matches. If those 50 appear in the first 200 records, the remaining 9,800 are never examined. With the array version, every record is always visited for the first filter pass regardless.
When to keep using array methods
Iterator helpers are not a universal replacement:
- You need random access: arrays let you index by position. An iterator moves forward only, once.
- You need to pass over the data multiple times: you can only consume an iterator once. Arrays are reusable.
-
You need
.sort(): sorting requires the full dataset in memory. There's no lazy sort. -
The list is small: for ten items, the overhead of
Iterator.from()is noise. Use whatever reads more clearly.
The clearest signal: if you're chaining array methods on a large or potentially infinite source and discarding intermediate results, that's the iterator helpers' use case.
TypeScript support
TypeScript 5.6 added the Iterator global type and the method signatures. If your tsconfig.json targets ES2024 or later, they're available automatically:
function* ids(): Generator<number> {
let id = 0;
while (true) yield id++;
}
const first10: number[] = Iterator.from(ids()).take(10).toArray();
Enter fullscreen mode Exit fullscreen mode
For earlier lib targets, you may need to add "ES2024" to the lib array explicitly, or use @types/core-js if you're polyfilling.
Browser support
Iterator helpers are Baseline 2024: Chrome 122 (March 2024), Firefox 131 (September 2024), Safari 18.2 (December 2024), Node.js 22. For environments that predate these versions, core-js 3.38+ includes a polyfill you can add to your bundle.
Generator functions already return an iterator — if you're writing generators today, their return values gain these methods automatically once your targets are modern enough.
🎮 Try it yourself
▶️ Open the interactive playground →
Runs right in your browser — poke at it and watch the concept react live.
🧠 Test yourself
Think it clicked? Take the 8-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
The takeaway
Search your codebase for .filter().map().slice(0, n) chains on arrays. Each one allocates intermediate arrays that go straight to the garbage collector. Replace them with Iterator.from() and .take(n), and the pipeline will stop as soon as it has what it needs — no extra work, no extra memory. The API surface is nearly identical to array methods. The only shift is thinking of the chain as a pipeline you're describing rather than a collection you're transforming.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email (work & contract inquiries): [email protected]
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.