iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0

「程式跑得出來只是基本,跑得快才是王道!」在實務開發中,效能問題更是影響用戶體驗的關鍵。

在 Day 15 我們完成了羅馬數字轉換器,功能雖然完整,但如果要處理大量轉換呢?今天讓我們用測試驅動的方式來優化效能 ⚡。

學習旅程 🗺️

今天我們將經歷四個階段:建立基準 → 快取機制 → 演算法改進 → 驗證比較

第一站:建立效能測試基準 📊

在進行任何優化前,我們需要先知道「現在有多慢」。就像減肥前要先量體重一樣,沒有基準就無法知道改善了多少。

撰寫效能測試

建立效能測試檔案:

// 建立 tests/day16/performance.test.ts
import { describe, test, expect } from 'vitest'
import { toRoman, fromRoman } from '../../src/roman/converter'

describe('Roman Numeral Performance Tests', () => {
  test('performanceBenchmark', () => {
    const start = performance.now()
    
    for (let i = 1; i <= 3999; i++) {
      toRoman(i)
    }
    
    const end = performance.now()
    const duration = end - start
    
    expect(duration).toBeLessThan(100)
    console.log(`toRoman: ${duration.toFixed(2)}ms for 3999 conversions`)
  })
})

紅燈 🔴:執行測試發現效能需要優化。

第二站:實作快取機制 💾

當我們發現同樣的數字會被重複轉換時,何不把結果記下來?這就是快取的概念 - 用空間換時間。

建立快取版本測試:

// 建立 tests/day16/cache-optimization.test.ts
import { describe, test, expect } from 'vitest'
import { toRomanCached, fromRomanCached, clearCache } from '../../src/roman/converter'

describe('Cached Roman Numeral Converter', () => {
  test('cacheRepeatedCalls', () => {
    clearCache()
    
    // 第一次呼叫
    const result1 = toRomanCached(1994)
    
    // 第二次呼叫相同數字應該更快
    const start = performance.now()
    const result2 = toRomanCached(1994)
    const duration = performance.now() - start
    
    expect(result1).toBe(result2)
    expect(result1).toBe('MCMXCIV')
    expect(duration).toBeLessThan(1) // 快取應該非常快
  })
})

紅燈 🔴:快取版本的函數不存在。

實作快取機制

實作快取版本:

// 更新 src/roman/converter.ts
// 快取物件
const toRomanCache = new Map<number, string>()

export function toRomanCached(num: number): string {
  if (toRomanCache.has(num)) {
    return toRomanCache.get(num)!
  }
  
  const result = toRoman(num)
  toRomanCache.set(num, result)
  
  return result
}

export function clearCache(): void {
  toRomanCache.clear()
}

綠燈 🟢:快取測試通過!

快取命中時,查詢時間從毫秒級降到微秒級。

第三站:查表法優化 📋

除了快取,我們還能從演算法本身下手。原本的實作使用了多個條件判斷,而查表法可以簡化這個過程。

建立查表法測試:

// 建立 tests/day16/lookup-optimization.test.ts
import { describe, test, expect } from 'vitest'
import { toRomanOptimized } from '../../src/roman/converter'

describe('Lookup Table Optimization', () => {
  test('optimizedConvertsCorrectly', () => {
    expect(toRomanOptimized(1)).toBe('I')
    expect(toRomanOptimized(1994)).toBe('MCMXCIV')
    expect(toRomanOptimized(3999)).toBe('MMMCMXCIX')
  })
  
  test('optimizedHandlesEdgeCases', () => {
    expect(() => toRomanOptimized(0)).toThrow()
    expect(() => toRomanOptimized(4000)).toThrow()
  })
})

紅燈 🔴:優化版本尚未實作。

實作查表演算法

實作優化版本:

// 更新 src/roman/converter.ts
const ROMAN_LOOKUP: Array<[number, string]> = [
  [1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],
  [100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],
  [10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']
]

export function toRomanOptimized(num: number): string {
  if (num <= 0 || num > 3999) {
    throw new Error('Number must be between 1 and 3999')
  }

  let result = ''
  let remaining = num
  
  for (const [value, symbol] of ROMAN_LOOKUP) {
    const count = Math.floor(remaining / value)
    if (count > 0) {
      result += symbol.repeat(count)
      remaining -= value * count
    }
  }
  
  return result
}

綠燈 🟢:優化測試通過!

查表法讓程式碼更簡潔,執行也更快。

第四站:效能比較測試 🏁

建立效能比較測試:

// 更新 tests/day16/performance.test.ts
test('bulkConversionComparison', () => {
  const testData = Array.from({ length: 500 }, (_, i) => i + 1)
  
  const start1 = performance.now()
  testData.forEach(num => toRoman(num))
  const duration1 = performance.now() - start1
  
  const start2 = performance.now()
  testData.forEach(num => toRomanOptimized(num))
  const duration2 = performance.now() - start2
  
  console.log(`Original: ${duration1.toFixed(2)}ms`)
  console.log(`Optimized: ${duration2.toFixed(2)}ms`)
  
  expect(duration2).toBeLessThan(duration1)
})

回歸測試:確保正確性 ✓

確保優化不破壞功能:

// 建立 tests/day16/regression.test.ts
test('allImplementationsIdentical', () => {
  const testCases = [1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000, 1994, 3999]
  
  for (const num of testCases) {
    const original = toRoman(num)
    const optimized = toRomanOptimized(num)
    const cached = toRomanCached(num)
    
    expect(optimized).toBe(original)
    expect(cached).toBe(original)
  }
})

完整優化實作 ✅

// 完整實作 src/roman/converter.ts
const toRomanCache = new Map<number, string>()

export function toRomanCached(num: number): string {
  if (toRomanCache.has(num)) {
    return toRomanCache.get(num)!
  }
  
  const result = toRomanOptimized(num)
  toRomanCache.set(num, result)
  return result
}

export function clearCache(): void {
  toRomanCache.clear()
}

測試執行與驗證 🧪

執行所有效能測試:

# 執行所有效能測試
npm test tests/day16/

# 執行特定的效能測試
npm test tests/day16/performance.test.ts

效能優化最佳實踐 🔧

1. 測量優先原則 📏

  • 先建立基準測試:知道起點在哪
  • 識別真正的瓶頸:80/20 法則
  • 避免過早優化:Donald Knuth 說過「過早優化是萬惡之源」

2. 快取策略選擇 💾

  • Map vs Object:Map 效能更佳
  • 選擇合適的資料結構:根據使用場景
  • 考慮記憶體限制:避免無限增長

3. 演算法優化技巧 🚀

  • 減少迴圈次數:能一次完成就不要兩次
  • 預計算常用值:空間換時間
  • 減少分支判斷:查表優於條件判斷

今天的收穫 🎯

今天我們使用 TDD 完成了羅馬數字轉換器的效能優化:

TDD 與效能優化 ⚡

  • 建立效能測試基準
  • 用測試驗證優化效果
  • 確保功能正確性不受影響

優化技術 🚀

  • 查表法減少運算複雜度
  • 快取機制避免重複計算
  • 演算法改進

測試策略 📊

  • 效能基準測試
  • 回歸測試確保正確性

小挑戰 🎮

試著實作 LRU(Least Recently Used)快取,限制快取大小到 100 個項目。當快取滿時,刪除最少使用的項目。

疑難排解指南 🛠️

常見效能問題

  1. 快取未生效
// 檢查快取是否正確初始化
console.log('Cache size:', toRomanCache.size)
  1. 記憶體洩漏
// 定期清理快取
if (toRomanCache.size > 10000) {
  toRomanCache.clear()
}

重點回顧 📝

今天我們學會了:

  1. 建立效能基準:先測量,再優化
  2. 快取機制:記住計算結果
  3. 演算法優化:查表法的威力
  4. 回歸測試:確保優化不破壞功能

明天我們將進行羅馬數字轉換器的最終整理與回顧,學習如何重構和改進程式碼架構,為這個階段做完美收尾 🎯。


上一篇
Day 15 - 羅馬數字反向轉換 🔄
下一篇
Day 17 - 程式碼整理與回顧 🏁
系列文
React TDD 實戰:用 Vitest 打造可靠的前端應用20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言