Skip to content

Performance Optimization in Template Rendering #3938

@mohammed-hassan-angular-frontend-fcmb

Description

Is your feature request related to a problem? Please describe.

Issue: There's a TODO comment about a hot inner loop in src/TemplateMap.js lines 328-336:

// TODO(slightlyoff): hot inner loop?
getMapEntryForInputPath(inputPath) {
let absoluteInputPath = TemplatePath.absolutePath(inputPath);
return this.map.find((entry) => {
if (entry.inputPath === inputPath || entry.inputPath === absoluteInputPath) {
return entry;
}
});
}

Performance Issues Identified

  1. O(n) Linear Search: The method uses Array.find() which has O(n) time complexity
  2. Called Frequently: This method is called in multiple hot paths:
  • cache() method: Line 280 - called for every template in orderedPaths.map()
  • populateCollectionsWithContent(): Line 482 - called for every collection item
  • initDependencyMap(): Line 185 - called for every dependency entry
  1. Various test scenarios and template processing workflows
    Expensive Path Resolution: TemplatePath.absolutePath() is called on every lookup, even though the result could be cached
  2. Scale Impact: With hundreds or thousands of templates, this becomes a significant bottleneck

Describe the solution you'd like

Proposed Solution

Replace the linear search with a Map-based lookup system:

  1. Add a lookup Map: Create this.inputPathMap alongside the existing this.map array
  2. Maintain both structures: Keep the array for iteration, add the Map for O(1) lookups.
  3. Handle both path formats: Store entries under both relative and absolute path keys.
  4. Update on modifications: Ensure the lookup Map stays in sync when entries are added

Describe alternatives you've considered

  1. Constructor Changes
    Add the lookup Map initialization:

constructor(eleventyConfig) {
if (!eleventyConfig || eleventyConfig.constructor.name !== "TemplateConfig") {
throw new Error("Missing or invalid eleventyConfig argument.");
}
this.eleventyConfig = eleventyConfig;
this.map = [];

// NEW: Add lookup Map for O(1) performance
this.inputPathMap = new Map();

this.collectionsData = null;
this.cached = false;
this.verboseOutput = true;
this.collection = new TemplateCollection();

}

  1. Enhanced add() Method
    Modify the add method to populate both structures:

async add(template) {
if (!template) {
return;
}

let data = await template.getData();
let entries = await template.getTemplateMapEntries(data);

for (let map of entries) {
    // Add to existing array (preserve existing functionality)
    this.map.push(map);
    
    // NEW: Add to lookup Map for fast access
    this._addToInputPathMap(map);
}

}

// NEW: Helper method to add entries to the lookup Map
_addToInputPathMap(mapEntry) {
const inputPath = mapEntry.inputPath;

// Store under the original inputPath
this.inputPathMap.set(inputPath, mapEntry);

// Also store under absolute path if different
const absoluteInputPath = TemplatePath.absolutePath(inputPath);
if (absoluteInputPath !== inputPath) {
    this.inputPathMap.set(absoluteInputPath, mapEntry);
}

}

  1. Optimized getMapEntryForInputPath() Method
    Replace the linear search with Map lookup:

// OPTIMIZED: O(1) lookup instead of O(n) search
getMapEntryForInputPath(inputPath) {
// Try direct lookup first
let entry = this.inputPathMap.get(inputPath);
if (entry) {
return entry;
}

// Try absolute path lookup if direct lookup failed
let absoluteInputPath = TemplatePath.absolutePath(inputPath);
return this.inputPathMap.get(absoluteInputPath);

}

  1. Add Benchmarking Support
    Add performance measurement to track improvements:

// NEW: Method to benchmark the performance improvement
_benchmarkLookupPerformance() {
if (this.map.length === 0) return;

const iterations = 1000;
const testPath = this.map[Math.floor(this.map.length / 2)].inputPath;

// Benchmark new method
const start = performance.now();
for (let i = 0; i < iterations; i++) {
    this.getMapEntryForInputPath(testPath);
}
const mapTime = performance.now() - start;

console.log(`TemplateMap lookup performance: ${iterations} lookups in ${mapTime.toFixed(2)}ms`);
console.log(`Average per lookup: ${(mapTime / iterations).toFixed(4)}ms`);

}

  1. Add Map Clearing Method
    For consistency and potential future use:

// NEW: Clear both data structures
clear() {
this.map = [];
this.inputPathMap.clear();
this.collectionsData = null;
this.cached = false;
}

  1. Add Validation Method
    For debugging and ensuring data integrity:

// NEW: Validate that both data structures are in sync (for debugging)
_validateMapConsistency() {
if (process.env.NODE_ENV === 'development') {
const mapPaths = new Set(this.map.map(entry => entry.inputPath));
const lookupPaths = new Set();

    for (let [path, entry] of this.inputPathMap) {
        lookupPaths.add(entry.inputPath);
    }
    
    const arrayOnly = [...mapPaths].filter(path => !lookupPaths.has(path));
    const mapOnly = [...lookupPaths].filter(path => !mapPaths.has(path));
    
    if (arrayOnly.length > 0 || mapOnly.length > 0) {
        console.warn('TemplateMap consistency issue:', { arrayOnly, mapOnly });
    }
}

}

Complete Updated Methods
Here's how the key methods would look after the optimization:

class TemplateMap {
#dependencyMapInitialized = false;

constructor(eleventyConfig) {
    if (!eleventyConfig || eleventyConfig.constructor.name !== "TemplateConfig") {
        throw new Error("Missing or invalid `eleventyConfig` argument.");
    }
    this.eleventyConfig = eleventyConfig;
    this.map = [];
    this.inputPathMap = new Map(); // NEW: O(1) lookup Map
    this.collectionsData = null;
    this.cached = false;
    this.verboseOutput = true;
    this.collection = new TemplateCollection();
}

async add(template) {
    if (!template) {
        return;
    }

    let data = await template.getData();
    let entries = await template.getTemplateMapEntries(data);

    for (let map of entries) {
        this.map.push(map);
        this._addToInputPathMap(map); // NEW: Add to lookup Map
    }
}

_addToInputPathMap(mapEntry) {
    const inputPath = mapEntry.inputPath;
    this.inputPathMap.set(inputPath, mapEntry);
    
    const absoluteInputPath = TemplatePath.absolutePath(inputPath);
    if (absoluteInputPath !== inputPath) {
        this.inputPathMap.set(absoluteInputPath, mapEntry);
    }
}

hasMapEntryForInputPath(inputPath) {
    return Boolean(this.getMapEntryForInputPath(inputPath));
}

// OPTIMIZED: O(1) instead of O(n)
getMapEntryForInputPath(inputPath) {
    let entry = this.inputPathMap.get(inputPath);
    if (entry) {
        return entry;
    }
    
    let absoluteInputPath = TemplatePath.absolutePath(inputPath);
    return this.inputPathMap.get(absoluteInputPath);
}

// ... rest of the class remains the same

}

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions