de Balaganesh Damodaran

Clasa Array JavaScript expune destul de multe metode (filtru, hartă, reducere), care iterează printr-o matrice și apelează o funcție iterator pentru a efectua acțiuni pe matrice. Înlănțuirea acestor metode vă permite să scrieți un cod curat, ușor de citit. Dar ce ne costă această comoditate în ceea ce privește performanța și merită?

Feriti va de inlantuirea metodelor de matrice in JavaScript

Javascript este un limbaj „funcțional”. Ceea ce înseamnă acest lucru este că funcțiile sunt obiecte de primă clasă în Javascript și, ca atare, pot fi transmise ca parametri altor funcții. Există destul de multe metode încorporate furnizate de biblioteca standard Javascript, care folosește acest fapt pentru a ne permite să scriem cod curat, ușor de înțeles și ușor de citit.

Metode încorporate de matrice Javascript și înlănțuire

O astfel de clasă încorporată care folosește pe scară largă natura funcțională a Javascript este Array clasă. Arrays în Javascript expune o serie de metode de instanță, care:

  • acceptați o funcție ca argument,
  • iterați pe matrice,
  • și apelați funcția, trecând de-a lungul elementului matricei ca parametru la funcție.

Cele mai populare dintre acestea sunt desigur forEach, filter, map și reduce. Deoarece unele dintre aceste metode returnează și Array ca valoare de returnare a metodei, acestea sunt adesea înlănțuite astfel:

const tripExpenses = [{    amount: 12.07,    currency: 'USD',    paid: true}, {    amount: 1.12,    currency: 'USD',    paid: true}, {    amount: 112.00,    currency: 'INR',    paid: false}, {    amount: 54.17,    currency: 'USD',    paid: true}, {    amount: 16.50,    currency: 'USD',    paid: true}, {    amount: 189.50,    currency: 'INR',    paid: false}];
const totalPaidExpensesInINR = tripExpenses    .filter(expense => expense.paid)    .map(expense => {        if(expense.currency == 'USD')            return expense.amount * 70;        else            return expense.amount;    })    .reduce((amountA, amountB) => amountA + amountB);

În acest exemplu, calculăm cheltuielile plătite totale după ce le-am convertit din USD în INR. Pentru a face acest lucru, suntem:

  • filtering tripExpenses să extragă doar cheltuielile plătite,
  • mapping suma cheltuielilor din moneda specificată și convertirea acesteia în INR și
  • reducesumele INR pentru a obține suma.

Pare un caz de utilizare obișnuit, foarte tipic, valid pentru înlănțuirea metodelor matrice nu? O mulțime de dezvoltatori cărora li s-a învățat să scrie Javascript funcțional ar ieși cu ceva similar atunci când li se va cere să rezolve această problemă.

Problema cu Înlănțuirea Metodei Array

În prezent, tripExpenses matricea are doar 6 articole, deci este relativ rapid. Dar ce se întâmplă atunci când trebuie să analizăm cheltuielile de călătorie pentru, să zicem, o întreagă companie de angajați pentru întregul exercițiu financiar și a noastră tripExpenses matricea începe să aibă sute de mii de elemente?

Datorită JSPerf, putem vizualiza acest cost destul de ușor. Deci, să rulați un test de comparație pentru același cod cu tripExpenses având 10 elemente, 10.000 de elemente și 100.000 de elemente. Iată rezultatul Comparație JSPerf:

Feriti va de inlantuirea metodelor de matrice in JavaScript

Graficul arată numărul de operații pe secundă, iar mai mare este mai bine. În timp ce mă așteptam ca cazul de 100.000 de elemente să nu aibă performanțe, chiar nu mă așteptam ca cazul de 10.000 de elemente să aibă performanțe slabe. Deoarece nu este vizibil cu adevărat pe grafic, să analizăm numerele:

  • 10 Elemente – 6.142.739 operații pe secundă
  • 10.000 de elemente – 2.199 operații pe secundă
  • 100.000 de elemente – 223 operațiuni pe secundă

Da, e foarte rău! Și, în timp ce procesarea unui set de 100.000 de elemente s-ar putea să nu se întâmple des, 10.000 de elemente este un caz de utilizare foarte plauzibil pe care l-am văzut în mod regulat în mai multe aplicații pe care le-am dezvoltat (mai ales pe partea serverului).

Acest lucru ne arată că atunci când scriem – chiar și ceea ce pare a fi un cod destul de simplu – ar trebui să fim cu atenție la orice problemă de performanță care ar putea apărea din cauza modului în care scriem codul nostru.

Dacă, în loc să înlănțuie filter, map și reduce împreună, ne rescriem codul astfel încât toată munca să fie realizată în linie, într-o singură buclă, să putem obține performanțe semnificativ mai bune.

let totalPaidExpensesInINR = 0;
for(let expense of tripExpenses){    if(expense.paid){        if(expense.currency == 'USD')            totalPaidExpensesInINR += (expense.amount * 70);        else            totalPaidExpensesInINR += expense.amount;    }}

Să alergăm altul Comparație JSPerf pentru a vedea cum funcționează acest lucru împotriva omologului său funcțional, într-un test de 10.000 de elemente:

1611359705 812 Feriti va de inlantuirea metodelor de matrice in JavaScript

După cum puteți vedea, pe Chrome (și prin extensie Node.JS), exemplul funcțional este cu 77% mai lent decât exemplul for-of. Pe Firefox, numerele sunt mult mai apropiate, dar exemplul funcțional este încă cu 16% mai lent decât exemplul for-of.

De ce o deltă de performanță atât de mare?

Deci, de ce este exemplul funcțional mult mai lent decât exemplul de exemplu? Ei bine, este o combinație de factori, dar factorii principali pe care, în calitate de dezvoltator, îi putem controla de pe teritoriul utilizatorilor sunt:

  • Buclarea asupra acelorși elemente de matrice de mai multe ori.
  • Cheltuielile generale ale funcției solicită fiecare iterație din exemplul funcțional.

Dacă vedeți exemplul de exemplu, veți vedea că vom itera numai prin tripExpenses matrice o dată. De asemenea, nu apelăm funcții din interior, ci efectuăm calculele noastre în linie.

Una dintre marile „câștiguri” de performanță pe care le obțin motoarele Javascript moderne este integrarea apelurilor funcționale. Ceea ce înseamnă acest lucru este că motorul vă va compila codul într-o versiune în care compilatorul înlocuiește apelul funcției, cu funcția în sine (adică în linie unde apelați funcția). Acest lucru elimină cheltuielile generale de apelare a funcției și oferă câștiguri imense de performanță.

Cu toate acestea, nu putem spune întotdeauna cu certitudine dacă un motor Javascript va alege să integreze sau nu o funcție, astfel încât să o facem singuri ne asigură că avem cea mai bună performanță posibilă.

Concluzie

Unii dezvoltatori pot considera că exemplul de mai jos este mai puțin lizibil și mai greu de înțeles decât exemplul funcțional. Pentru acest exemplu particular, aș spune că ambele stiluri sunt la fel de lizibile. Cu toate acestea, în cazul exemplului funcțional, confortul înlănțuirii metodei tinde să ascundă multiplele iterații și apeluri de funcții de la dezvoltator, făcând astfel mai ușor pentru un dezvoltator neexperimentat să scrie cod neperformant.

Nu spun că ar trebui să evitați întotdeauna modul funcțional – sunt sigur că există o mulțime de cazuri valabile pentru utilizarea modului funcțional și pentru înlănțuirea metodelor. Dar o regulă generală de reținut care trebuie amintită atunci când vine vorba de performanță și iterarea matricilor în Javascript este că, dacă înlănțuiți metode care iterează prin întreaga matrice, probabil că ar trebui să vă opriți și să luați în considerare impactul performanței înainte de a merge mai departe.

Mi-ar plăcea să vă aud părerea despre ceea ce am scris în acest articol. Cântă cu comentariile tale de mai jos.

[Feb 6th, 2019] Unele avertismente și lucruri de reținut, așa cum au subliniat comentatorii

La fel de Evidențiat de Paul B, există o lovitură de performanță atunci când se folosește `pentru … of` într-o formă transpilată în browsere, dar puteți folosi oricând o buclă normală cu o variabilă iterator pentru a evita acest lucru. Cu toate acestea, așa cum spune Paul, există destul de multe avantaje în a rămâne cu o funcție de iterare. Mergi să citești comentariul lui, este demn să fii un articol singur.

În plus, o mulțime de oameni au spus, de asemenea, că aceasta ar fi o optimizare prematură sau o micro-optimizare și sunt parțial de acord cu ei. În general, ar trebui să optimizați întotdeauna pentru lizibilitate și mentenabilitate față de performanță, până în momentul în care performanța slabă începe să vă afecteze. Odată ce ați atins acest punct, vă recomandăm să vă reconsiderați iteratorii.

Publicat inițial la asleepysamurai.com pe 8 ianuarie 2019.