Skip to content

Performance ​

This guide covers performance optimization strategies for jpgx, including memory management, batch processing, and parallel operations.

Encoding Performance ​

Quality Impact ​

Quality has minimal impact on encoding speed:

typescript
import { encode } from 'jpgx'

// Both have similar encoding time
const low = encode(imageData, 30)   // ~50ms
const high = encode(imageData, 95)  // ~52ms

// Output size differs significantly
console.log('Low:', low.data.length)   // ~50KB
console.log('High:', high.data.length) // ~400KB

Image Size Impact ​

Encoding time scales with pixel count:

typescript
// Rough estimates for encoding time
// 640x480 (307K pixels):  ~10ms
// 1280x720 (921K pixels): ~30ms
// 1920x1080 (2M pixels):  ~70ms
// 3840x2160 (8M pixels):  ~280ms

Decoding Performance ​

Output Format Impact ​

typescript
// RGBA output (default)
const rgba = decode(jpegData, { formatAsRGBA: true })
// Uses width * height * 4 bytes

// RGB output (25% less memory)
const rgb = decode(jpegData, { formatAsRGBA: false })
// Uses width * height * 3 bytes

Typed Arrays ​

typescript
// Enable for slightly faster processing
const image = decode(jpegData, {
  useTArray: true,
})

Memory Optimization ​

Limit Resolution ​

Prevent memory exhaustion:

typescript
// Safe defaults for web
const image = decode(jpegData, {
  maxResolutionInMP: 25,     // 5000x5000 max
  maxMemoryUsageInMB: 256,
})

Memory Estimation ​

typescript
function estimateMemory(width: number, height: number, rgba = true): number {
  const bytesPerPixel = rgba ? 4 : 3
  const bytes = width * height * bytesPerPixel
  return Math.ceil(bytes / (1024 * 1024)) // MB
}

// Check before decoding
const headers = parseJpegHeaders(jpegData)
const estimatedMB = estimateMemory(headers.width, headers.height)

if (estimatedMB > 256) {
  console.warn(`Image would use ~${estimatedMB}MB of memory`)
}

Stream-Like Processing ​

Process images without loading all at once:

typescript
async function processImageBatch(paths: string[], maxConcurrent = 3) {
  const results = []

  for (let i = 0; i < paths.length; i += maxConcurrent) {
    const batch = paths.slice(i, i + maxConcurrent)

    const batchResults = await Promise.all(
      batch.map(async (path) => {
        const data = await Bun.file(path).arrayBuffer()
        const image = decode(new Uint8Array(data), {
          maxMemoryUsageInMB: 128,
        })

        const processed = processImage(image)
        return encode(processed, 75)
      })
    )

    results.push(...batchResults)

    // Allow GC between batches
    await Bun.sleep(0)
  }

  return results
}

Batch Processing ​

Sequential Processing ​

typescript
async function processSequential(images: Uint8Array[]): Promise<Uint8Array[]> {
  const results: Uint8Array[] = []

  for (const image of images) {
    const decoded = decode(image)
    const processed = applyFilter(decoded)
    const encoded = encode(processed, 75)
    results.push(encoded.data)
  }

  return results
}

Parallel Processing ​

typescript
async function processParallel(
  images: Uint8Array[],
  concurrency = 4
): Promise<Uint8Array[]> {
  const results: Uint8Array[] = new Array(images.length)
  let nextIndex = 0

  async function worker() {
    while (nextIndex < images.length) {
      const index = nextIndex++
      const decoded = decode(images[index])
      const processed = applyFilter(decoded)
      results[index] = encode(processed, 75).data
    }
  }

  const workers = Array.from({ length: concurrency }, () => worker())
  await Promise.all(workers)

  return results
}

Caching ​

Result Caching ​

typescript
const cache = new Map<string, { data: Uint8Array; timestamp: number }>()
const CACHE*TTL = 5 * 60 * 1000 // 5 minutes

function cachedEncode(
  imageData: RawImageData,
  quality: number
): Uint8Array {
  const key = `${imageData.width}x${imageData.height}:${quality}:${
    Bun.hash(imageData.data).toString()
  }`

  const cached = cache.get(key)
  if (cached && Date.now() - cached.timestamp < CACHE*TTL) {
    return cached.data
  }

  const result = encode(imageData, quality)

  cache.set(key, {
    data: result.data,
    timestamp: Date.now(),
  })

  return result.data
}

LRU Cache ​

typescript
class LRUCache<T> {
  private cache = new Map<string, T>()

  constructor(private maxSize: number) {}

  get(key: string): T | undefined {
    const value = this.cache.get(key)
    if (value !== undefined) {
      this.cache.delete(key)
      this.cache.set(key, value)
    }
    return value
  }

  set(key: string, value: T): void {
    if (this.cache.has(key)) {
      this.cache.delete(key)
    }
    else if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)
    }
    this.cache.set(key, value)
  }
}

const imageCache = new LRUCache<Uint8Array>(100)

Benchmarking ​

Measure Performance ​

typescript
async function benchmark(
  imagePath: string,
  iterations = 100
) {
  const data = await Bun.file(imagePath).arrayBuffer()
  const jpegData = new Uint8Array(data)

  // Benchmark decode
  let decodeStart = performance.now()
  for (let i = 0; i < iterations; i++) {
    decode(jpegData)
  }
  let decodeTime = (performance.now() - decodeStart) / iterations

  // Benchmark encode
  const decoded = decode(jpegData)
  let encodeStart = performance.now()
  for (let i = 0; i < iterations; i++) {
    encode(decoded, 75)
  }
  let encodeTime = (performance.now() - encodeStart) / iterations

  console.log(`Image: ${decoded.width}x${decoded.height}`)
  console.log(`Decode: ${decodeTime.toFixed(2)}ms`)
  console.log(`Encode: ${encodeTime.toFixed(2)}ms`)
  console.log(`Total: ${(decodeTime + encodeTime).toFixed(2)}ms`)
}

Compare Settings ​

typescript
async function compareQualities(imagePath: string) {
  const data = await Bun.file(imagePath).arrayBuffer()
  const image = decode(new Uint8Array(data))

  const qualities = [30, 50, 70, 85, 95, 100]

  console.log('Quality | Size (KB) | Time (ms)')
  console.log('--------|-----------|----------')

  for (const quality of qualities) {
    const start = performance.now()
    const result = encode(image, quality)
    const time = performance.now() - start

    console.log(
      `${quality.toString().padStart(7)} | ` +
      `${Math.round(result.data.length / 1024).toString().padStart(9)} | ` +
      `${time.toFixed(2).padStart(8)}`
    )
  }
}

Best Practices ​

  1. Choose appropriate quality: Higher quality = larger files
  2. Use RGB when alpha not needed: Saves 25% memory
  3. Set memory limits: Prevent crashes
  4. Process in batches: Better memory management
  5. Cache when possible: Avoid redundant processing
  6. Profile before optimizing: Measure first

Next Steps ​

Released under the MIT License.