Executando verificação de segurança...
Em resposta a Veja além do óbvio
5

Excelente!

Esse é o tipo de coisa que muita gente não se preocupa, porque na maioria das vezes estão lidando com arrays pequenos e a diferença não é perceptível. Afinal, para poucos dados, tudo é rápido. O problema só aparece em grandes volumes, e é aí que conhecer melhor os algoritmos e estruturas de dados pode fazer toda a diferença.

Indo um pouco além, console.time é uma maneira rápida de medir o tempo de execução, mas tem alguns problemas (leia para mais detalhes, não é o foco aqui se alongar neste tópico). Enfim, um jeito de contornar esses problemas é usando uma lib dedicada, como o Benchmark.js (deixei um exemplo mais abaixo).

E aproveitando, dá pra melhorar ainda mais (se o desempenho for um fator crítico e estiver lidando com muitos dados). Métodos como map e filter recebem como parâmetro uma função de callback que é chamada para todos os elementos. Apesar de não fazer tanta diferença para poucos arrays pequenos, ainda sim chamadas de função têm o seu custo, e para muitos dados pode fazer diferença. Então a ideia é eliminar estas chamadas:

function filterForOf(emails, users) {
    const usersEmailsSet = new Set();
    for (const u of users) { // em vez de map, um for..of
        usersEmailsSet.add(u.email);
    }
    const diff = [];
    for (const email of emails) { // em vez de filter, um for..of
        if (!usersEmailsSet.has(email)) {
            diff.push(email);
        }
    }
    return diff;
}

E mais ainda: for..of é conveniente, mas também tem custo maior do que o for "tradicional". Então vamos fazer outra versão:

function filterFor(emails, users) {
    const usersEmailsSet = new Set();
    for (var i = 0; i < users.length; i++) { // for "tradicional" em vez de for..of
        usersEmailsSet.add(users[i].email);
    }
    const diff = [];
    for (var i = 0; i < emails.length; i++) { // for "tradicional" em vez de for..of
        if (!usersEmailsSet.has(emails[i])) {
            diff.push(emails[i]);
        }
    }
    return diff;
}

Enfim, fiz um teste com todas estas opções usando o Benchmark.js. O código completo:

var Benchmark = require('benchmark');
var suite = new Benchmark.Suite;

const { faker } = require('@faker-js/faker');

const emails = [];
const users = [];
Array.from({ length: 50000 }, (_, i) => i).forEach(() => {
    const email = faker.internet.email({ provider: 'fake' });
    emails.push(email);
    users.push({ email });
});

Array.from({ length: 10000 }, (_, i) => i).forEach(() => {
    const email = faker.internet.email({ provider: 'fake' });
    emails.push(email);
});

function filterWithSome(emails, users) {
    return emails.filter((email) => !users.some((user) => user.email === email));
}

function filterWithSet(emails, users) {
    const usersEmailsSet = new Set(users.map((user) => user.email));
    const difference = emails.filter((email) => !usersEmailsSet.has(email));
    return difference;
}

function filterForOf(emails, users) {
    const usersEmailsSet = new Set();
    for (const u of users) {
        usersEmailsSet.add(u.email);
    }
    const diff = [];
    for (const email of emails) {
        if (!usersEmailsSet.has(email)) {
            diff.push(email);
        }
    }
    return diff;
}
function filterFor(emails, users) {
    const usersEmailsSet = new Set();
    for (var i = 0; i < users.length; i++) {
        usersEmailsSet.add(users[i].email);
    }
    const diff = [];
    for (var i = 0; i < emails.length; i++) {
        if (!usersEmailsSet.has(emails[i])) {
            diff.push(emails[i]);
        }
    }
    return diff;
}

suite
.add('filterWithSome', function () {
    filterWithSome(emails, users);
})
.add('filterWithSet', function () {
    filterWithSet(emails, users);
})
.add('filterForOf', function () {
    filterForOf(emails, users);
})
.add('filterFor', function () {
    filterFor(emails, users);
})
.on('complete', function () {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
})
.on('cycle', function (event) {
    console.log(String(event.target));
})
.run({ 'async': true });

Ele mostra a quantidade de operações por segundo ("ops/sec"), ou seja, quanto mais, melhor. O resultado foi:

filterWithSome x 0.18 ops/sec ±18.44% (5 runs sampled)
filterWithSet x 158 ops/sec ±2.53% (81 runs sampled)
filterForOf x 179 ops/sec ±3.48% (76 runs sampled)
filterFor x 205 ops/sec ±2.54% (87 runs sampled)
Fastest is filterFor

Ou seja, o for tradicional foi mais rápido, seguido do for..of. Aqui dá pra ver que eliminar os callbacks fez diferença. Rodei mais algumas vezes e os resultados foram consistentes, com pouca variação nos números.

Ah, e também tem a opção de usar um objeto simples em vez de Set:

function filterObject(emails, users) {
    const usersEmails = {};
    for (var i = 0; i < users.length; i++) {
        usersEmails[users[i].email] = true;
    }

    const diff = [];
    for (var i = 0; i < emails.length; i++) {
        if (!usersEmails[emails[i]]) {
            diff.push(emails[i]);
        }
    }
    return diff;
}

Assim, cada email se torna uma propriedade do mesmo (e caso o email se repita, ele sobrescreve a mesma propriedade, ou seja, "já cuida dos valores duplicados").
Nos meus testes, esta solução se mostrou um pouco pior que filterWithSet, mas ainda sim bem melhor que filterWithSome. O que me parece é que a implementação do Set está mais otimizada do que o lookup das propriedades de um objeto.


Por curiosidade, a partir do Node 22 (e nas versões recentes dos principais browsers) é possível calcular a diferença entre dois Set's usando o método difference. Ou seja, também poderia ser assim:

function filterSetsDiff(emails, users) {
    const usersEmailsSet = new Set();
    for (var i = 0; i < users.length; i++) {
        usersEmailsSet.add(users[i].email);
    }
    // método difference funciona a partir do Node 22
    return [...new Set(emails).difference(usersEmailsSet)];
}

Porém, esta não se mostrou melhor que o for. Nos meus testes ela ficou um pouco pior que o filterObject (mas ainda é bem melhor que filterWithSome). Provavelmente porque precisa criar também o Set de emails, depois o método difference retorna outro Set, e por fim este é convertido para array. Esta volta toda acaba tendo um custo que neste caso parece não compensar.

Carregando publicação patrocinada...
2