Bolzplatzarena

Der Schlüssel zum Glück - lodash Performance

Wer kennt das nicht. Man läd viele Daten aus der Datenbank oder von einer Api und danach muss ich die Daten manipulieren oder einfach nur immer wieder draufzugreifen. Das kann man natürlich mit einem "find" machen, aber man kann die Daten auch für sich aufbereiten, damit der Zugriff gut und schnell Funktioniert. Wir kennen das unter dem Stichwort Dictionary.

Für den Test wiurden einfach zufallsdaten generiert, die am Ende auch dafür genutzt wurden, zu verifizieren, dass die Daten am Ende identisch sind. Es wäre ja fatal wenn wir Birnen mit Äpfeln vergleichen. Bei den Projekten, welche wir bearbeiten, sind solche Datenmengen (2000) nicht selten, meist geht es aber darüber hinaus.

example-data.ts

typescript
1 2 3 4 5
const array = [...Array(2000)
  .keys()].map(item => ({
  id: item,
  value: Math.floor(Math.random() * 100),
}));

Lodash bietet eine einfach Methode, die auch noch einleuchtet benannt ist: "keyBy". Das bedeutet, ich komme aus einer Liste, Schlüssel-Wert-Paare.

lodash.ts

typescript
1
keyBy(array, item => item.id)

Ich lasse den Test 1000 mal abspielen und lodash benötigt knapp 82ms um die Daten zu berechnen. Gar nicht so schlecht. Wir machen diese Aktion zwar häufig aber weit unter 1000 mal.

Dennoch prüft ich ob das nicht schneller gehen kann. Kurz gegooglt und da findet man schnell wie man keyBy nativ ersetzen kann.

key-by.ts

typescript
1
const keyBy = (data: any[], key: string) => (data || []).reduce((r, x) => ({ ...r, [key ? x[key] : x]: x }), {});

Unter der Annahme, dass das nun schneller sein sollte, habe ich das Testszenario so belassen. Aber die Annahme war falsch, nach mehreren Sekunden stoppte ich den Test. Ein kurzer Blick in die Methode verrät ein kniffliges Detail. Der Spreadoperator. Das ist ein nettes Feature und ermöglicht einfache Kopien, aber in diesem Szenario wird er überstraperziert. Denn zunächst wird ein Array mit einem Element gespreadet, dann eine Array mit 2 Elementen, dann ... nun man kann sich vorstellen wie das weiter geht.

Nun wollte ich mich langsam rantasten an einen Vergleichswert für die erste Methode. Mal einen Durchlauf machen und dann auf den Wert von lodash hochrechnen, klang nach einer tollen Idee. Das Ergebnis war aber ernüchternd. 1 einziger Durchlauf, lag schon bei 350ms. Ich habe dann natüclich noch kurz mit 2 Testdurchäufen geprüft und lag bei 650ms. Zur Erinnung loash brauchte 82ms ... für 1000 Durchläufe.

Lohnt dennoch ein Blick darauf ob es besser geht? zunächst schaut man in die Implementierung von lodashs keyBy und stellt fest, dass sie reduce benutzen. Allerdings in den nicht nativen Variante. Da wir gelernt haben, dass reduce nativ gar nicht mal so schlecht ist, überlebt dieser Teil der schlechten Methode. Den Rest machen wir dann doch anders:

native-key-by.ts

typescript
1 2 3 4 5 6 7
function nativeKeyBy<T extends { [index: string]: number }>(data: T[], key: string): Dictionary<T> {
  return (data || []).reduce((prev: Dictionary<T>, newValue: T) => {
    prev[newValue[key]] = newValue;

    return prev;
  }, {});
}

Entschuldigt meinen schlechten Stil ;)

Ergebnis? Genügend Vertrauen in den Code habe ich, also trauen wir dem Browser direkt die 1000 Durchläufe zu? Natürlich.

Bähm. 46ms. Zur Erinnerung lodash benötigt 82ms. Aber wir sind immer noch bei den 1000 Durchläufen. Von daher ist das nicht zwangsläufig ein Aus für die lodash-Variante. Diese ist sicherlich flexibler und hat daher einiges an Overhead zu bieten und sollte deshalb wiederhin in Betracht gezogen werden.