
懶有懶的好處,聊聊 Lazy Evaluation 
故事發生於某天平凡的午後,鱈魚我和平常一樣快樂地用著 Remeda,使用 take 時忽然很好奇原始碼如何實作。
旁白:「『未看先猜一定是 `Array.slice` 啦!』鱈魚邊說邊打開程式碼,結果出現火星文,看不懂的大笨魚只能失落的挺著肥肚肚,望之興嘆。」
鱈魚:「給我照腳本念,不要偷嘴我!ლ(´口`ლ)」 
讓我們看看 Remeda 的 take 原始碼:
export function take(...args: ReadonlyArray<unknown>): unknown {
  return purry(takeImplementation, args, lazyImplementation)
}
function takeImplementation<T extends IterableContainer>(array: T, n: number): Array<T[number]> {
  return n < 0 ? [] : array.slice(0, n)
}
function lazyImplementation<T>(n: number): LazyEvaluator<T> {
  if (n <= 0) {
    return lazyEmptyEvaluator
  }
  let remaining = n
  return (value) => {
    remaining -= 1
    return { done: remaining <= 0, hasNext: true, next: value }
  }
}purry 是 Remeda 特殊函數,不在此文章討論範圍,有興趣的朋友可以來這裡看看。
可以看到的確有 Array.slice 沒錯,不過還有 lazyImplementation 部分,這個其實就是實作「惰性求值」(Lazy Evaluation)邏輯。
至於甚麼是「惰性求值」?簡單來說,就是在需要時才計算,而不是一開始就計算所有東西。
細節網路上超多文章,這裡就不贅述了。(ゝ∀・)y
來 Bench 一下 
讓我們來 Bench 一下,感受看看差異吧。
邏輯為:將一個巨大的矩陣 map、filter 後,只取前 5 筆資料。
資料定義如下:
export const mapFn = (x: number): number => x * 2
export const filterFn = (x: number): boolean => x % 3 === 0
export const TAKE_COUNT = 5
const DATA_SIZE = 1_000_000
export const data = Array.from({ length: DATA_SIZE }, (_, i) => i + 1)總共測試三種版本程式:
Remeda 的 lazy 版本:
lazy.ts
import { filter, map, pipe, take } from 'remeda'
pipe(
  data,
  map(mapFn),
  filter(filterFn),
  take(TAKE_COUNT)
)原生 Array 版本:
native.ts
data
  .map(mapFn)
  .filter(filterFn)
  .slice(0, TAKE_COUNT)將 Remeda function 拆開來用,不要使用 lazy:
no-lazy.ts
import { filter, map, take } from 'remeda'
const mapped = map(data, mapFn)
const filtered = filter(mapped, filterFn)
take(filtered, TAKE_COUNT)可以看到結果差距非常驚人。
| Name | Ops/sec | 
|---|---|
| lazy.ts | 1,113,819 Best | 
| no-lazy.ts | 63 (-100.0%) | 
| native.ts | 61 (-100.0%) | 
- lazy版本表現最佳,達到每秒 111 萬次操作,遠超過其他兩個版本。
- no-lazy與- native版本表現相近很合理,因為- Remeda的- map、- filter都是使用原生的- Array.map、- Array.filter。
lazy 版本會那麼快,是因為它只需要計算到滿足 take 的部分,並不會計算整個資料集,所以時間才差那麼多。
所以用 Remeda 性能都會好棒棒嗎? 
倒也不能這麼說,讓我們去掉 take 的部分,再跑一次看看。
| Name | Ops/sec | 
|---|---|
| lazy.ts | 19(-70.4%) | 
| no-lazy.ts | 64 Best | 
| native.ts | 63(-1.8%) | 
可以看到 lazy 的效能大幅下降,甚至比不上 no-lazy 版本。
這是因為 lazy 的 take 只在需要時才計算,當沒有 take 時,整個資料集都需要計算,這樣就失去了惰性求值的優勢,反而會因為 lazy 額外的計算邏輯,導致效能下降。
或者把資料數量改成 50 筆,再跑一次。
| Name | Ops/sec | 
|---|---|
| lazy.ts | 1,017,423 (-36.0%) | 
| no-lazy.ts | 1,539,932 (-3.2%) | 
| native.ts | 1,590,550 Best | 
可以看到 lazy 版本下降惹 Σ(ˊДˋ;)
這是因為瀏覽器為了 JS 性能操碎了心,這點資料才不算甚麼,lazy 的計算邏輯反而拖累了性能。
原生不能懶懶的嗎? 
可以使用 generator 來實作惰性求值邏輯:
generator.ts
function* mapIterator<T, U>(
  iterable: Iterable<T>,
  mapFn: (item: T) => U
): IterableIterator<U> {
  for (const item of iterable) {
    yield mapFn(item)
  }
}
function* filterIterator<T>(
  iterable: Iterable<T>,
  filterFn: (item: T) => boolean
): IterableIterator<T> {
  for (const item of iterable) {
    if (filterFn(item)) {
      yield item
    }
  }
}
function* takeIterator<T>(
  iterable: Iterable<T>,
  count: number
): IterableIterator<T> {
  let i = 0
  for (const item of iterable) {
    if (i++ >= count)
      break
    yield item
  }
}
export function run() {
  const iterator = takeIterator(
    filterIterator(
      mapIterator(data, mapFn),
      filterFn
    ),
    TAKE_COUNT
  )
  const result = Array.from(iterator)
}Bench 結果:
| Name | Ops/sec | 
|---|---|
| lazy.ts | 636,538 (-0.2%) | 
| no-lazy.ts | 40 (-100.0%) | 
| native.ts | 40 (-100.0%) | 
| generator.ts | 637,597 Best | 
也有 proposal-iterator-helpers 提案,截至 2025/05/26 還在 Stage 4,雖然已經有瀏覽器實作,不過要注意支援度。
iIterator.ts
function* toIterator<T>(array: T[]) {
  for (const val of array) {
    yield val
  }
}
export function run() {
  toIterator(data)
    .map(mapFn)
    .filter(filterFn)
    .take(TAKE_COUNT)
    .toArray()
}Bench 結果:
| Name | Ops/sec | 
|---|---|
| lazy.ts | 586,274 Best | 
| no-lazy.ts | 30 (-100.0%) | 
| native.ts | 34 (-100.0%) | 
| generator.ts | 501,987 (-14.4%) | 
| iterator.ts | 545,057 (-7.0%) | 
以上就是這次的簡單測試。੭ ˙ᗜ˙ )੭
鱈魚:「以後遇到懶得做的 feature,就說我正在 Lazy Evaluation 就對惹。(ゝ∀・)b」
PM:(抽出生魚片刀 ( ˘•ω•˘ ))
總結 🐟 
- Remeda的- lazy版本在處理大量資料時,能夠避免不必要的計算,提升效能。
- 不過當資料量小時,lazy反而會拖慢效能。
- 性能會因情境、runtime、瀏覽器最佳化與程式寫法而有所不同,最好應該實際跑過 benchmark 再做選擇。