JavaScript flatMap() vs using forEach() to flatten arrays

Summary

We investigated the performance and readability differences between using flatMap() and a forEach() loop with concat() to flatten an array of objects. The core issue is that while both methods achieve the same result, flatMap() is significantly more performant and idiomatic for this specific use case. The forEach() approach creates intermediate arrays and iterates multiple times, leading to unnecessary memory pressure and slower execution, especially on large datasets.

Root Cause

The root cause of inefficiency lies in how each method handles array construction and memory allocation.

  • flatMap() Implementation: JavaScript engines optimize flatMap() to perform the mapping and flattening in a single pass, avoiding the creation of intermediate array structures.
  • forEach() + concat() Implementation:
    • concat() creates a new array every time it is called. It does not modify the original array.
    • In the loop allItems = allItems.concat(menu.items), a new array is allocated for every iteration (e.g., for every menu item).
    • This leads to quadratic memory allocation overhead as the array grows, forcing the garbage collector to clean up discarded intermediate arrays.

Why This Happens in Real Systems

Developers often default to forEach() or for loops because they are familiar and explicitly show iteration logic. This pattern is common in legacy codebases or when developers are less familiar with ES6+ functional methods.

  • Habit: forEach is a “go-to” loop for many, even when a specialized method exists.
  • Legacy Compatibility: Before flatMap() was standardized in ES2019, the forEach + push/concat pattern was the standard way to flatten arrays manually.
  • Misunderstanding of concat(): Junior developers often miss that concat() returns a new array rather than modifying the existing one in place.

Real-World Impact

Using the forEach + concat pattern in high-throughput environments can lead to:

  • Performance Bottlenecks: In data-intensive applications (e.g., processing large JSON responses), the overhead of repeated array allocation can cause noticeable lag and increase CPU usage.
  • Memory Pressure: Generating excessive short-lived arrays triggers the Garbage Collector (GC) more frequently. In Node.js or browser environments, this can cause “stop-the-world” GC pauses, affecting UI responsiveness or server request latency.
  • Reduced Readability: The flatMap() method conveys the intent (map and flatten) immediately, whereas the forEach version requires manual parsing to understand the data transformation.

Example or Code

Both examples below produce the same result: [1, 2, 3, 4].

The Efficient Approach (flatMap)

const menus = [
  { name: 'Menu1', items: [1, 2] },
  { name: 'Menu2', items: [3, 4] }
];

const allItems = menus.flatMap(menu => menu.items);

The Inefficient Approach (forEach + concat)

const menus = [
  { name: 'Menu1', items: [1, 2] },
  { name: 'Menu2', items: [3, 4] }
];

let allItems = [];
menus.forEach(menu => {
  // This creates a new array on every iteration
  allItems = allItems.concat(menu.items);
});

How Senior Engineers Fix It

Senior engineers prioritize performance and maintainability. They fix this by:

  • Using Built-in Optimizations: They leverage flatMap() because it is semantically correct and optimized by the JavaScript engine (V8, SpiderMonkey, etc.).
  • Avoiding Intermediate Arrays: If flatMap() is not applicable (e.g., complex flattening logic), they use reduce() or a simple for loop with push() to modify an accumulator array in place.
  • Code Reviews: They enforce strict linting rules (e.g., no-loops) or use code review comments to discourage manual array flattening when a standard method exists.

Key Takeaway: Always prefer declarative methods (flatMap, reduce) over imperative loops for data transformation to ensure code clarity and runtime efficiency.

Why Juniors Miss It

Junior developers often miss this optimization due to a lack of deep understanding of JavaScript internals:

  • Focus on Logic, Not Performance: They see forEach + concat as a “working solution” and stop there, without considering the cost of memory allocation.
  • Unfamiliarity with ES6+: They may not know flatMap() exists or understand that it combines mapping and flattening.
  • Misconception about Mutability: They might think concat is efficient because “it looks clean,” not realizing it copies data.
  • Debugging Difficulty: The performance impact is subtle in small datasets (like the example), so they don’t encounter the problem until they work with large-scale data in production.

Key Takeaway: Junior developers often prioritize immediate readability (familiar loops) over long-term performance (optimized native methods), missing the hidden cost of memory allocation.