Skip to content

Memory optimization for image-heavy builds #302

@jornmineur

Description

@jornmineur

*EDIT Jan 26, 2026: a PR that resolves this issue is ready for review.


Summary

On a website with:

  • 1200+ unique images,
  • 600+ pages using standard eleventy-img image shortcodes (no WebC),
  • multiple responsive formats (AVIF, JPEG) and sizes per image,

... memory usage reaches 90GB, causing memory swapping and system freezes.

Setup:

  • OS: macOS 15.5 (M2 Max, 32GB RAM)
  • Node: v22.11.0
  • @11ty/eleventy-img: 6.0.1
  • @11ty/eleventy: 3.1.2

With a custom patch, memory footprint was reduced by 96% and build time by 60%.

Suggestion is to add a similar patch to eleventy-img.

Alternative workarounds tried

Changing concurrency settings in eleventy-img or old space settings in V8 has no effect.

Root Cause Analysis

  1. Buffer Caching: Image.#contents stores source image buffers without eviction
  2. Instance Caching: memCache retains all Image instances for the entire build lifecycle
  3. Linear Growth: Memory usage grows linearly with unique images processed
// Current behavior in Image class
class Image {
  #contents = {};
  
  async getFileContents() {
    // Buffers are cached indefinitely
    if (!this.#contents[src]) {
      this.#contents[src] = await fs.readFile(src);
    }
    return this.#contents[src];
  }
}

Expected Behavior

Memory usage should remain bounded regardless of the number of images processed, especially when:

  • Images are processed once and outputs are reused
  • The same responsive image formats/sizes are generated consistently
  • Disk cache already exists for processed images

Proposed Solution

Add an optional manifest-based caching system that:

  1. Tracks processed images with metadata (dimensions, formats, URLs)
  2. Detects changes via file modification time/size
  3. Skips reprocessing for unchanged images
  4. Reduces memory by avoiding unnecessary Image instantiation

Suggested API

// Option 1: Global configuration
eleventyConfig.addPlugin(eleventyImagePlugin, {
  manifestPath: ".cache/image-manifest.json",
  enableManifest: process.env.NODE_ENV === "production"
});

// Option 2: Per-image configuration
await Image(src, {
  widths: [300, 600],
  formats: ["avif", "jpeg"],
  useManifest: true,
  manifestPath: ".cache/image-manifest.json"
});

Manifest Structure

{
  "./src/images/hero.jpg": {
    "fingerprint": {
      "mtime": 1234567890,
      "size": 123456
    },
    "outputs": {
      "avif": [
        { "width": 300, "height": 200, "filename": "xYz123-300.avif" },
        { "width": 600, "height": 400, "filename": "xYz123-600.avif" }
      ],
      "jpeg": [
        { "width": 300, "height": 200, "filename": "xYz123-300.jpeg" },
        { "width": 600, "height": 400, "filename": "xYz123-600.jpeg" }
      ]
    }
  }
}

Proof of Concept Results

Results of a custom implementation wrapping eleventy-img:

Metric Before After Improvement
Memory Usage 90GB 3.2GB -96%
Build Time (cached) 5 min 2 min -60%

Backwards Compatibility

  • Manifest usage would be opt-in
  • Existing behavior unchanged without explicit configuration
  • Falls back gracefully if manifest is corrupted/missing

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions