Group by

Benchmark created on


Preparation HTML

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>

Setup

// Lodash-based implementation that returns a Map with groupKey omitted
function groupByLodash(array) {
    const grouped = _.groupBy(array, 'groupKey');
    const map = new Map();
    
    for (const [key, items] of Object.entries(grouped)) {
        map.set(key, items.map(item => {
            const { groupKey, ...rest } = item;
            return rest;
        }));
    }
    
    return map;
}

// Native implementation using Map with groupKey omitted
function groupByNativeMap(array) {
    const map = new Map();
    
    for (const item of array) {
        const key = item.groupKey;
        
        if (!map.has(key)) {
            map.set(key, []);
        }
        
        const { groupKey, ...rest } = item;
        map.get(key).push(rest);
    }
    
    return map;
}

// Optimized native implementation with pre-allocated arrays
function groupByNativeOptimized(array) {
    const map = new Map();
    
    // First pass: count items per group
    const counts = new Map();
    for (const item of array) {
        const key = item.groupKey;
        counts.set(key, (counts.get(key) || 0) + 1);
    }
    
    // Pre-allocate arrays with known sizes
    for (const [key, count] of counts) {
        map.set(key, []);
    }
    
    // Second pass: populate arrays
    for (const item of array) {
        const { groupKey, ...rest } = item;
        map.get(groupKey).push(rest);
    }
    
    return map;
}

// Alternative using Object.create(null) for faster property access
function groupByNativeObject(array) {
    const groups = Object.create(null);
    
    for (const item of array) {
        const key = item.groupKey;
        
        if (!groups[key]) {
            groups[key] = [];
        }
        
        const { groupKey, ...rest } = item;
        groups[key].push(rest);
    }
    
    // Convert to Map
    const map = new Map();
    for (const key in groups) {
        map.set(key, groups[key]);
    }
    
    return map;
}

// Alternative using Object.create(null) for faster property access
function createNativeObject(array) {
    const groups = Object.create(null);
    
    for (const item of array) {
        const key = item.groupKey;
        
        if (!groups[key]) {
            groups[key] = [];
        }
        
        const { groupKey, ...rest } = item;
        groups[key].push(rest);
    }
    return groups;
}

// Optimized version avoiding destructuring overhead
function groupByNativeNoDest(array) {
    const map = new Map();
    
    for (const item of array) {
        const key = item.groupKey;
        
        if (!map.has(key)) {
            map.set(key, []);
        }
        
        // Manual property copying (avoiding destructuring overhead)
        const newItem = {};
        for (const prop in item) {
            if (prop !== 'groupKey') {
                newItem[prop] = item[prop];
            }
        }
        
        map.get(key).push(newItem);
    }
    
    return map;
}

// Version using Object.assign
function groupByNativeAssign(array) {
    const map = new Map();
    
    for (const item of array) {
        const key = item.groupKey;
        
        if (!map.has(key)) {
            map.set(key, []);
        }
        
        // Use Object.assign and delete
        const newItem = Object.assign({}, item);
        delete newItem.groupKey;
        
        map.get(key).push(newItem);
    }
    
    return map;
}

// Generate test data: 50,000 groups with 1-4 objects each
function generateTestData() {
    const data = [];
    const numGroups = 50000;
    
    for (let groupId = 0; groupId < numGroups; groupId++) {
        // Each group has 1-4 items
        const groupSize = Math.floor(Math.random() * 4) + 1;
        const groupKey = `group_${groupId}`;
        
        for (let j = 0; j < groupSize; j++) {
            data.push({
                groupKey: groupKey,
                id: `${groupId}_${j}`,
                name: `Item_${groupId}_${j}`,
                value: Math.floor(Math.random() * 1000),
                timestamp: Date.now() - (groupId * 1000),
                category: `cat_${groupId % 100}`,
                score: Math.random(),
                active: Math.random() > 0.5
            });
        }
    }
    
    // Shuffle the array to simulate real-world unsorted data
    for (let i = data.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [data[i], data[j]] = [data[j], data[i]];
    }
    
    return data;
}

// Generate the test data once
const testData = generateTestData();
console.log(`Generated ${testData.length} items across ~500,000 groups`);

// Smaller test set for quick validation
const smallTestData = testData.slice(0, 1000);

Test runner

Ready to run.

Testing in
TestOps/sec
Lodash groupBy
// Test Case 1: Lodash-based Map implementation
groupByLodash(testData);
ready
Native with Map
// Test Case 2: Native Map implementation
groupByNativeMap(testData);

ready
Native map optimised
// Test Case 3: Native Optimized implementation
groupByNativeOptimized(testData);
ready
Objects with create(null)
// Test Case 4: Native Object->Map implementation
groupByNativeObject(testData);

ready
Native Object -> Map
// Test Case 4: Native Object->Map implementation
groupByNativeObject(testData);

ready
Native without destructuring
// Test Case 5: Native without destructuring
groupByNativeNoDest(testData);
ready
Native with Object.assign
// Test Case 6: Native with Object.assign
groupByNativeAssign(testData);
ready
Create just native object
createNativeObject(testData);
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.