TagLib-Wasm
Guide
API Reference
Examples
NPM
GitHub
Guide
API Reference
Examples
NPM
GitHub
  • Documentation

    • taglib-wasm API Reference
    • TagLib Tag Name Constants and Cross-Format Mapping
    • Extended Metadata with PropertyMap API
    • Runtime Compatibility
    • Memory Management
    • Performance Guide
    • Error Handling Guide
    • Implementation Guide
    • Troubleshooting Guide
    • Publishing Guide
    • Cloudflare Workers

Performance Guide

This guide covers performance optimization techniques and best practices for taglib-wasm.

Table of Contents

  • Memory Management
  • Processing Optimization
  • Batch Operations
  • Runtime-Specific Optimizations
  • Benchmarking
  • Performance Tips

Memory Management

Memory Configuration

Choose the right memory configuration based on your use case:

// Small files (< 10MB) - Default configuration
const taglib = await TagLib.initialize();

// Medium files (10-50MB)
const taglib = await TagLib.initialize({
  memory: {
    initial: 32 * 1024 * 1024, // 32MB
    maximum: 128 * 1024 * 1024, // 128MB
  },
});

// Large files (> 50MB)
const taglib = await TagLib.initialize({
  memory: {
    initial: 64 * 1024 * 1024, // 64MB
    maximum: 256 * 1024 * 1024, // 256MB
  },
});

// Memory-constrained environments (e.g., Cloudflare Workers)
const taglib = await TagLib.initialize({
  memory: {
    initial: 8 * 1024 * 1024, // 8MB
    maximum: 8 * 1024 * 1024, // Fixed size
  },
});

Memory Usage Patterns

Understanding memory usage helps optimize performance:

// Memory usage breakdown:
// - Base Wasm module: ~2-4MB
// - Per file overhead: ~2x file size
// - Peak usage during save: ~3x file size

function estimateMemoryUsage(fileSizeMB: number): number {
  const baseOverhead = 4; // MB
  const fileOverhead = fileSizeMB * 2;
  const peakOverhead = fileSizeMB * 3;

  return {
    minimum: baseOverhead + fileOverhead,
    peak: baseOverhead + peakOverhead,
  };
}

// Example: 10MB MP3 file
// Minimum: 4MB + 20MB = 24MB
// Peak: 4MB + 30MB = 34MB

Memory Pooling

Reuse TagLib instances for better performance:

class TagLibPool {
  private instance: TagLib | null = null;
  private initPromise: Promise<TagLib> | null = null;

  async getInstance(): Promise<TagLib> {
    if (this.instance) {
      return this.instance;
    }

    if (!this.initPromise) {
      this.initPromise = TagLib.initialize({
        memory: {
          initial: 32 * 1024 * 1024,
          maximum: 256 * 1024 * 1024,
        },
      });
    }

    this.instance = await this.initPromise;
    return this.instance;
  }
}

// Global pool
const pool = new TagLibPool();

// Usage
async function processFile(buffer: Uint8Array) {
  const taglib = await pool.getInstance();
  const file = taglib.openFile(buffer);
  try {
    // Process...
  } finally {
    file.dispose();
  }
}

Processing Optimization

Minimize File Operations

// ❌ Inefficient: Multiple operations
const file1 = taglib.openFile(buffer);
file1.setTitle("Title");
file1.dispose();

const file2 = taglib.openFile(buffer);
file2.setArtist("Artist");
file2.dispose();

const file3 = taglib.openFile(buffer);
file3.setAlbum("Album");
file3.dispose();

// ✅ Efficient: Single operation
const file = taglib.openFile(buffer);
file.setTitle("Title");
file.setArtist("Artist");
file.setAlbum("Album");
file.dispose();

Lazy Loading

Only read what you need:

// Simple API - automatically optimized
const tags = await readTags(buffer); // Only reads tags
const props = await readProperties(buffer); // Only reads properties

// Core API - manual optimization
class LazyAudioFile {
  private file: AudioFile;
  private _tags?: TagData;
  private _props?: AudioProperties;

  constructor(file: AudioFile) {
    this.file = file;
  }

  get tags(): TagData {
    if (!this._tags) {
      this._tags = this.file.tag();
    }
    return this._tags;
  }

  get properties(): AudioProperties {
    if (!this._props) {
      this._props = this.file.audioProperties();
    }
    return this._props;
  }

  dispose() {
    this.file.dispose();
  }
}

Selective Tag Updates

Only update changed fields:

class SmartTagger {
  private original: TagData;
  private file: AudioFile;

  constructor(file: AudioFile) {
    this.file = file;
    this.original = file.tag();
  }

  updateTags(updates: Partial<TagData>) {
    let hasChanges = false;

    if (updates.title && updates.title !== this.original.title) {
      this.file.setTitle(updates.title);
      hasChanges = true;
    }

    if (updates.artist && updates.artist !== this.original.artist) {
      this.file.setArtist(updates.artist);
      hasChanges = true;
    }

    if (updates.album && updates.album !== this.original.album) {
      this.file.setAlbum(updates.album);
      hasChanges = true;
    }

    // Only save if there were changes
    if (hasChanges) {
      return this.file.save();
    }

    return true;
  }
}

Batch Operations

Sequential Processing

Best for memory-constrained environments:

async function processSequentially(files: string[]) {
  const taglib = await TagLib.initialize();
  const results = [];

  for (const filePath of files) {
    const buffer = await Deno.readFile(filePath);
    const file = taglib.openFile(buffer);

    try {
      const result = {
        path: filePath,
        tags: file.tag(),
        properties: file.audioProperties(),
      };
      results.push(result);
    } finally {
      file.dispose();
    }

    // Optional: Force garbage collection between files
    if (globalThis.gc) {
      globalThis.gc();
    }
  }

  return results;
}

Parallel Processing

Best for CPU-bound operations with sufficient memory:

async function processInParallel(files: string[], concurrency = 4) {
  const taglib = await TagLib.initialize({
    memory: {
      initial: 64 * 1024 * 1024,
      maximum: 512 * 1024 * 1024, // Larger for parallel processing
    },
  });

  // Process in chunks
  const chunks = [];
  for (let i = 0; i < files.length; i += concurrency) {
    chunks.push(files.slice(i, i + concurrency));
  }

  const results = [];
  for (const chunk of chunks) {
    const chunkResults = await Promise.all(
      chunk.map(async (filePath) => {
        const buffer = await Deno.readFile(filePath);
        const file = taglib.openFile(buffer);

        try {
          return {
            path: filePath,
            tags: file.tag(),
            properties: file.audioProperties(),
          };
        } finally {
          file.dispose();
        }
      }),
    );

    results.push(...chunkResults);
  }

  return results;
}

Stream Processing

For very large collections:

async function* streamProcess(files: string[]) {
  const taglib = await TagLib.initialize();

  for (const filePath of files) {
    try {
      const buffer = await Deno.readFile(filePath);
      const file = taglib.openFile(buffer);

      const result = {
        path: filePath,
        tags: file.tag(),
        properties: file.audioProperties(),
      };

      file.dispose();
      yield result;
    } catch (error) {
      yield {
        path: filePath,
        error: error.message,
      };
    }
  }
}

// Usage
for await (const result of streamProcess(files)) {
  console.log(result);
  // Process immediately, don't accumulate in memory
}

Runtime-Specific Optimizations

Deno Optimizations

// Use Deno's native file operations
const buffer = await Deno.readFile(path); // Efficient native read

// Use Workers for CPU-intensive tasks
const worker = new Worker(
  new URL("./tag-worker.ts", import.meta.url).href,
  { type: "module" },
);

worker.postMessage({ cmd: "process", buffer });

// tag-worker.ts
self.onmessage = async (e) => {
  const { cmd, buffer } = e.data;
  if (cmd === "process") {
    const taglib = await TagLib.initialize();
    const file = taglib.openFile(buffer);
    const tags = file.tag();
    file.dispose();
    self.postMessage({ tags });
  }
};

Node.js Optimizations

import { Worker } from "worker_threads";
import { createReadStream } from "fs";
import { pipeline } from "stream/promises";

// Use streams for large files
async function processLargeFile(path: string) {
  const chunks: Buffer[] = [];
  const stream = createReadStream(path);

  stream.on("data", (chunk) => chunks.push(chunk));
  await new Promise((resolve, reject) => {
    stream.on("end", resolve);
    stream.on("error", reject);
  });

  const buffer = Buffer.concat(chunks);
  return processBuffer(new Uint8Array(buffer));
}

// Use Worker threads
const worker = new Worker("./tag-worker.js");
worker.postMessage({ buffer });

Browser Optimizations

// Use Web Workers
const worker = new Worker("tag-worker.js");

// Process file uploads efficiently
async function handleFileUpload(file: File) {
  // Use streams for large files
  if (file.size > 50 * 1024 * 1024) { // > 50MB
    const reader = file.stream().getReader();
    const chunks: Uint8Array[] = [];

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      chunks.push(value);
    }

    const buffer = new Uint8Array(
      chunks.reduce((acc, chunk) => acc + chunk.length, 0),
    );

    let offset = 0;
    for (const chunk of chunks) {
      buffer.set(chunk, offset);
      offset += chunk.length;
    }

    return buffer;
  } else {
    // Small files can be read at once
    return new Uint8Array(await file.arrayBuffer());
  }
}

// Cache Wasm module
let cachedModule: TagLib | null = null;

async function getCachedTagLib(): Promise<TagLib> {
  if (!cachedModule) {
    cachedModule = await TagLib.initialize();
  }
  return cachedModule;
}

Cloudflare Workers Optimizations

// Optimize for Workers constraints
export default {
  async fetch(request: Request): Promise<Response> {
    // Initialize once per request
    const taglib = await TagLib.initialize({
      memory: {
        initial: 8 * 1024 * 1024, // 8MB limit
        maximum: 8 * 1024 * 1024, // Fixed size
      },
    });

    // Stream response for large files
    const { readable, writable } = new TransformStream();
    const writer = writable.getWriter();

    // Process in background
    (async () => {
      try {
        const buffer = new Uint8Array(await request.arrayBuffer());
        const file = taglib.openFile(buffer);

        const metadata = {
          tags: file.tag(),
          properties: file.audioProperties(),
        };

        file.dispose();

        await writer.write(
          new TextEncoder().encode(JSON.stringify(metadata)),
        );
      } finally {
        await writer.close();
      }
    })();

    return new Response(readable, {
      headers: { "Content-Type": "application/json" },
    });
  },
};

Benchmarking

Performance Measurement

class PerformanceMonitor {
  private metrics = new Map<string, number[]>();

  async measure<T>(
    name: string,
    operation: () => Promise<T>,
  ): Promise<T> {
    const start = performance.now();

    try {
      return await operation();
    } finally {
      const duration = performance.now() - start;

      if (!this.metrics.has(name)) {
        this.metrics.set(name, []);
      }
      this.metrics.get(name)!.push(duration);
    }
  }

  getStats(name: string) {
    const times = this.metrics.get(name) || [];
    if (times.length === 0) return null;

    const sorted = [...times].sort((a, b) => a - b);
    const sum = sorted.reduce((a, b) => a + b, 0);

    return {
      count: times.length,
      min: sorted[0],
      max: sorted[sorted.length - 1],
      avg: sum / times.length,
      median: sorted[Math.floor(times.length / 2)],
      p95: sorted[Math.floor(times.length * 0.95)],
      p99: sorted[Math.floor(times.length * 0.99)],
    };
  }

  report() {
    console.log("Performance Report:");
    for (const [name, _] of this.metrics) {
      const stats = this.getStats(name);
      console.log(`\n${name}:`);
      console.log(`  Count: ${stats.count}`);
      console.log(`  Min: ${stats.min.toFixed(2)}ms`);
      console.log(`  Avg: ${stats.avg.toFixed(2)}ms`);
      console.log(`  Median: ${stats.median.toFixed(2)}ms`);
      console.log(`  P95: ${stats.p95.toFixed(2)}ms`);
      console.log(`  P99: ${stats.p99.toFixed(2)}ms`);
      console.log(`  Max: ${stats.max.toFixed(2)}ms`);
    }
  }
}

// Usage
const monitor = new PerformanceMonitor();

// Benchmark initialization
await monitor.measure("init", () => TagLib.initialize());

// Benchmark file operations
const taglib = await TagLib.initialize();
for (const file of testFiles) {
  const buffer = await Deno.readFile(file);

  await monitor.measure("open", () => {
    const f = taglib.openFile(buffer);
    f.dispose();
    return Promise.resolve();
  });

  await monitor.measure("read_tags", () => {
    const f = taglib.openFile(buffer);
    const tags = f.tag();
    f.dispose();
    return Promise.resolve(tags);
  });

  await monitor.measure("write_tags", () => {
    const f = taglib.openFile(buffer);
    f.setTitle("New Title");
    f.save();
    f.dispose();
    return Promise.resolve();
  });
}

monitor.report();

Memory Profiling

class MemoryProfiler {
  private baseline: number;
  private samples: Array<{ label: string; usage: number }> = [];

  constructor() {
    this.baseline = this.getCurrentMemory();
  }

  private getCurrentMemory(): number {
    if (typeof process !== "undefined" && process.memoryUsage) {
      return process.memoryUsage().heapUsed;
    } else if (performance.memory) {
      return performance.memory.usedJSHeapSize;
    }
    return 0;
  }

  sample(label: string) {
    const current = this.getCurrentMemory();
    const delta = current - this.baseline;

    this.samples.push({
      label,
      usage: delta,
    });

    return delta;
  }

  report() {
    console.log("Memory Profile:");
    console.log(`Baseline: ${(this.baseline / 1024 / 1024).toFixed(2)}MB`);

    for (const sample of this.samples) {
      const mb = sample.usage / 1024 / 1024;
      const sign = mb >= 0 ? "+" : "";
      console.log(`${sample.label}: ${sign}${mb.toFixed(2)}MB`);
    }

    const peak = Math.max(...this.samples.map((s) => s.usage));
    console.log(`\nPeak delta: +${(peak / 1024 / 1024).toFixed(2)}MB`);
  }
}

// Usage
const profiler = new MemoryProfiler();

const taglib = await TagLib.initialize();
profiler.sample("After init");

const buffer = await Deno.readFile("large-file.flac");
profiler.sample("After file read");

const file = taglib.openFile(buffer);
profiler.sample("After file open");

const tags = file.tag();
profiler.sample("After tag read");

file.dispose();
profiler.sample("After dispose");

profiler.report();

Performance Tips

1. Choose the Right API

// Simple API - Best for:
// - Single operations
// - Automatic memory management
// - Quick scripts
const tags = await readTags("song.mp3");

// Core API - Best for:
// - Batch operations
// - Memory control
// - Performance-critical applications
const taglib = await TagLib.initialize();
// ... reuse for multiple files

2. Optimize File Size Handling

// For small files (< 10MB): Load entire file
const smallBuffer = await Deno.readFile("small.mp3");
const tags = await readTags(smallBuffer);

// For large files (> 50MB): Consider alternatives
// - Process metadata only (first/last few MB)
// - Use streaming if only reading
// - Increase memory limits

3. Minimize Allocations

// ❌ Creates multiple buffers
async function inefficient(files: string[]) {
  for (const file of files) {
    const data = await Deno.readFile(file);
    const copy = new Uint8Array(data); // Unnecessary copy
    const tags = await readTags(copy);
  }
}

// ✅ Reuses buffers where possible
async function efficient(files: string[]) {
  for (const file of files) {
    const data = await Deno.readFile(file);
    const tags = await readTags(data); // Direct use
  }
}

4. Profile Before Optimizing

// Always measure first
console.time("operation");
const result = await expensiveOperation();
console.timeEnd("operation");

// Then optimize based on data
if (executionTime > threshold) {
  // Apply optimization
}

5. Consider Caching

class MetadataCache {
  private cache = new Map<string, { tags: Tags; timestamp: number }>();
  private maxAge = 3600000; // 1 hour

  async getTags(path: string): Promise<Tags> {
    const cached = this.cache.get(path);

    if (cached && Date.now() - cached.timestamp < this.maxAge) {
      return cached.tags;
    }

    const tags = await readTags(path);
    this.cache.set(path, { tags, timestamp: Date.now() });

    return tags;
  }

  clear() {
    this.cache.clear();
  }
}
Edit this page on GitHub
Last Updated:: 6/15/25, 3:46 AM
Contributors: Charles Wiltgen
Prev
Memory Management
Next
Error Handling Guide