window.pipedriveLeadboosterConfig = { base: 'leadbooster-chat.pipedrive.com', companyId: 11580370, playbookUuid: '22236db1-6d50-40c4-b48f-8b11262155be', version: 2, } ;(function () { var w = window if (w.LeadBooster) { console.warn('LeadBooster już istnieje') } else { w.LeadBooster = { q: [], on: function (n, h) { this.q.push({ t: 'o', n: n, h: h }) }, trigger: function (n) { this.q.push({ t: 't', n: n }) }, } } })() Kilka sztuczek przyspieszających działanie aplikacji JavaScript - The Codest
The Codest
  • O nas
  • Nasze Usługi
    • Software Development
      • Frontend Development
      • Backend Development
    • Zespoły IT
      • Programiści frontendowi
      • Backend Dev
      • Inżynierowie danych
      • Inżynierowie rozwiązań chmurowych
      • Inżynierowie QA
      • Inne
    • Konsultacje IT
      • Audyt i doradztwo
  • Branże
    • Fintech i bankowość
    • E-commerce
    • Adtech
    • Healthtech
    • Produkcja
    • Logistyka
    • Motoryzacja
    • IOT
  • Wartość dla
    • CEO
    • CTO
    • Delivery Managera
  • Nasz zespół
  • Case Studies
  • Nasze Know How
    • Blog
    • Meetups
    • Webinary
    • Raporty
Kariera Skontaktuj się z nami
  • O nas
  • Nasze Usługi
    • Software Development
      • Frontend Development
      • Backend Development
    • Zespoły IT
      • Programiści frontendowi
      • Backend Dev
      • Inżynierowie danych
      • Inżynierowie rozwiązań chmurowych
      • Inżynierowie QA
      • Inne
    • Konsultacje IT
      • Audyt i doradztwo
  • Wartość dla
    • CEO
    • CTO
    • Delivery Managera
  • Nasz zespół
  • Case Studies
  • Nasze Know How
    • Blog
    • Meetups
    • Webinary
    • Raporty
Kariera Skontaktuj się z nami
Strzałka w tył WSTECZ
2020-11-25
Software Development

Kilka sztuczek przyspieszających działanie aplikacji JavaScript

The Codest

Bartosz Słysz

Software Engineer

Wraz z rozwojem technologii przeglądarek, aplikacje internetowe zaczęły przenosić coraz więcej logiki do front-endu, odciążając tym samym serwer i zmniejszając liczbę operacji, które musi wykonać. W podstawowych CRUD-ach rola serwera sprowadza się do autoryzacji, walidacji, komunikacji z bazami danych i wymaganej logiki biznesowej. Reszta logiki danych, jak się okazuje, może być łatwo obsłużona przez kod odpowiedzialny za reprezentację aplikacji po stronie UI.

W tym artykule postaram się pokazać kilka przykładów i wzorów, które pomogą nam utrzymać nasze zdrowie. kod wydajny, schludny i szybki.

Zanim zagłębimy się w konkretne przykłady - w tym artykule chciałbym skupić się jedynie na pokazaniu przypadków, które moim zdaniem mogą w zaskakujący sposób wpłynąć na szybkość działania aplikacji. Nie oznacza to jednak, że stosowanie szybszych rozwiązań jest najlepszym wyborem w każdym możliwym przypadku. Poniższe wskazówki należy raczej traktować jako coś, na co warto zwrócić uwagę, gdy nasza aplikacja działa wolno, np. w produktach wymagających renderowania gier lub bardziej zaawansowanych wykresów na płótnie, operacji wideo lub działań, które chcemy jak najszybciej zsynchronizować w czasie rzeczywistym.

Po pierwsze - metody Array.prototype

Dużą część logiki aplikacji opieramy na tablicach - ich mapowaniu, sortowaniu, filtrowaniu, sumowaniu elementów itd. W łatwy, przejrzysty i naturalny sposób korzystamy z ich wbudowanych metod, które po prostu pozwalają nam wykonywać różnego rodzaju obliczenia, grupowania itp. Działają one podobnie w każdej instancji - jako argument przekazujemy funkcję, gdzie w większości przypadków wartość elementu, indeks i tablica są kolejno przesuwane podczas każdej iteracji. Podana funkcja jest wykonywana dla każdego elementu w tablicy, a wynik jest różnie interpretowany w zależności od metody. Nie będę rozwodził się nad metodami Array.prototype, ponieważ chcę skupić się na tym, dlaczego w dużej liczbie przypadków działa ona wolno.

Metody Array są powolne, ponieważ wykonują funkcję dla każdego elementu. Funkcja wywoływana z perspektywy silnika musi przygotować nowe wywołanie, zapewnić odpowiedni zakres i wiele innych zależności, co sprawia, że proces jest o wiele dłuższy niż powtórzenie konkretnego bloku kodu w konkretnym zakresie. I to chyba wystarczająca wiedza podstawowa, która pozwoli nam zrozumieć poniższy przykład:

(() => {
const randomArray = [...Array(1E6).keys()].map(() => ({ value: Math.random() }));

console.time('Sum by reduce');
const reduceSum = randomArray
    .map(({ wartość }) => wartość)
    .reduce((a, b) => a + b);
console.timeEnd('Sum by reduce');

console.time('Sum by for loop');
let forSum = randomArray[0].value;
for (let index = 1; index < randomArray.length; index++) {
    forSum += randomArray[index].value;
}
console.timeEnd('Suma przez pętlę for');

console.log(reduceSum == forSum);

})();

Wiem, że ten test nie jest tak wiarygodny jak benchmarki (wrócimy do nich później), ale uruchamia lampkę ostrzegawczą. Dla losowego przypadku na moim komputerze okazuje się, że kod z pętlą for może być około 50 razy szybszy, jeśli porównać go do mapowania, a następnie zmniejszania elementów, które osiągają ten sam efekt! Chodzi o operowanie na jakimś dziwnym obiekcie stworzonym tylko po to, by osiągnąć konkretny cel obliczeń. Stwórzmy więc coś bardziej uzasadnionego, aby być obiektywnym co do metod Array:

(() => {
const randomArray = [...Array(1E6).keys()].map(() => ({ value: Math.random() }));

console.time('Sum by reduce');
const reduceSum = randomArray
    .reduce((a, b) => ({ value: a.value + b.value }).value
console.timeEnd('Sum by reduce');

console.time('Sum by for loop');
let forSum = randomArray[0].value;
for (let index = 1; index < randomArray.length; index++) {
    forSum += randomArray[index].value;
}
console.timeEnd('Suma przez pętlę for');

console.log(reduceSum == forSum);

})();

Wiem, że ten test nie jest tak wiarygodny jak benchmarki (wrócimy do nich później), ale uruchamia lampkę ostrzegawczą. Dla losowego przypadku na moim komputerze okazuje się, że kod z pętlą for może być około 50 razy szybszy w porównaniu do mapowania, a następnie redukcji elementów, które osiągają ten sam efekt! Dzieje się tak, ponieważ uzyskanie sumy w tym konkretnym przypadku przy użyciu metody reduce wymaga mapowania tablicy dla czystych wartości, które chcemy podsumować. Stwórzmy więc coś bardziej uzasadnionego, aby być obiektywnym co do metod Array:

(() => {
const randomArray = [...Array(1E6).keys()].map(() => ({ value: Math.random() }));

console.time('Sum by reduce');
const reduceSum = randomArray
    .reduce((a, b) => ({ value: a.value + b.value }).value
console.timeEnd('Sum by reduce');

console.time('Sum by for loop');
let forSum = randomArray[0].value;
for (let index = 1; index < randomArray.length; index++) {
    forSum += randomArray[index].value;
}
console.timeEnd('Suma przez pętlę for');

console.log(reduceSum == forSum);

})();

I jak się okazuje, nasz boost 50x zmniejszył się do boostu 4x. Przepraszamy, jeśli czujesz się rozczarowany! Aby pozostać obiektywnym do końca, przeanalizujmy jeszcze raz oba kody. Po pierwsze - niewinnie wyglądające różnice podwoiły spadek naszej teoretycznej złożoności obliczeniowej; zamiast najpierw mapować, a potem sumować czyste elementy, wciąż operujemy na obiektach i konkretnym polu, by na końcu wyciągnąć interesującą nas sumę. Problem pojawia się, gdy na kod spojrzy inny programista - wtedy, w porównaniu do kodów pokazanych wcześniej, ten drugi w pewnym momencie traci na abstrakcyjności.

Dzieje się tak dlatego, że od drugiej operacji operujemy na dziwnym obiekcie, z interesującym nas polem i drugim, standardowym obiektem iterowanej tablicy. Nie wiem co Ty o tym myślisz, ale z mojej perspektywy, w drugim przykładzie kodu, logika pętli for jest o wiele bardziej przejrzysta i intencjonalna niż ta dziwnie wyglądająca redukcja. I choć nie jest to już mityczne 50, to wciąż jest to 4 razy szybciej, jeśli chodzi o czas obliczeń! Ponieważ każda milisekunda jest cenna, wybór w tym przypadku jest prosty.

Najbardziej zaskakujący przykład

Druga rzecz, którą chciałem porównać dotyczy metody Math.max, a dokładniej wypchania miliona elementów i wyciągnięcia z nich największego i najmniejszego. Przygotowałem kod, metody pomiaru czasu również, po czym odpalam kod i dostaję bardzo dziwny błąd - przekroczono rozmiar stosu. Oto kod:

(() => {
const randomValues = [...Array(1E6).keys()].map(() => Math.round(Math.random() * 1E6) - 5E5);

console.time('Math.max z operatorem rozrzutu ES6');
const maxBySpread = Math.max(...randomValues);
console.timeEnd('Math.max z operatorem rozrzutu ES6');

console.time('Math.max z pętlą for');
let maxByFor = randomValues[0];
for (let index = 1; index  maxByFor) {
        maxByFor = randomValues[index];
    }
}
console.timeEnd('Math.max z pętlą for');

console.log(maxByFor == maxBySpread);

})();
(() => {
const randomValues = [...Array(1E6).keys()].map(() => Math.round(Math.random() * 1E6) - 5E5);

console.time('Math.max z operatorem rozrzutu ES6');
const maxBySpread = Math.max(...randomValues);
console.timeEnd('Math.max z operatorem rozrzutu ES6');

console.time('Math.max z pętlą for');
let maxByFor = randomValues[0];
for (let index = 1; index  maxByFor) {
        maxByFor = randomValues[index];
    }
}
console.timeEnd('Math.max z pętlą for');

console.log(maxByFor == maxBySpread);

})();

Okazuje się, że metody natywne korzystają z rekurencji, która w v8 jest ograniczona przez stosy wywołań, a jej liczba jest zależna od środowiska. Jest to coś, co bardzo mnie zaskoczyło, ale niesie za sobą wniosek: metoda natywna jest szybsza, o ile nasza tablica nie przekracza pewnej magicznej liczby elementów, którą w moim przypadku okazało się 125375. Dla tej liczby elementów wynik for był 5x szybszy w porównaniu do pętli. Jednak powyżej wspomnianej liczby elementów pętla for zdecydowanie wygrywa - w przeciwieństwie do przeciwnika pozwala nam uzyskać poprawne wyniki.

Rekursja

Pojęciem, o którym chcę wspomnieć w tym akapicie, jest rekurencja. W poprzednim przykładzie widzieliśmy ją w metodzie Math.max i składaniu argumentów, gdzie okazało się, że niemożliwe jest uzyskanie wyniku dla wywołań rekurencyjnych przekraczających określoną liczbę ze względu na ograniczenie rozmiaru stosu.

Zobaczymy teraz, jak wygląda rekurencja w kontekście kodu napisanego w JS, a nie za pomocą wbudowanych metod. Być może najbardziej klasyczną rzeczą, jaką możemy tutaj pokazać, jest oczywiście znalezienie n-tego wyrazu w ciągu Fibonacciego. Napiszmy więc to!

(() => {
const fiboIterative = (n) => {
let [a, b] = [0, 1];

    for (let i = 0; i  {
    if(n < 2) {
        return n;
    }

    return fiboRecursive(n - 2) + fiboRecursive(n - 1);
};

console.time('Ciąg Fibonacciego przez pętlę for');
const resultIterative = fiboIterative(30);
console.timeEnd('Ciąg Fibonacciego przez pętlę for');

console.time('Ciąg Fibonacciego przez rekurencję');
const resultRecursive = fiboRecursive(30);
console.timeEnd('Ciąg Fibonacciego przez rekurencję');

console.log(resultRecursive == resultIterative);

})();

Okej, w tym konkretnym przypadku obliczania 30. elementu sekwencji na moim komputerze, otrzymujemy wynik w około 200 razy krótszym czasie za pomocą algorytmu iteracyjnego.

Jest jednak jedna rzecz, którą można poprawić w algorytmie rekurencyjnym - jak się okazuje, działa on znacznie wydajniej, gdy zastosujemy taktykę zwaną tail recursion. Oznacza to, że przekazujemy wynik, który uzyskaliśmy w poprzedniej iteracji, używając argumentów do głębszych wywołań. Pozwala nam to zmniejszyć liczbę wymaganych wywołań, a w rezultacie przyspieszyć wynik. Poprawmy odpowiednio nasz kod!

(() => {
const fiboIterative = (n) => {
let [a, b] = [0, 1];

    for (let i = 0; i  {
    if(n == 0) {
        return first;
    }

    return fiboTailRecursive(n - 1, second, first + second);
};

console.time('Ciąg Fibonacciego przez pętlę for');
const resultIterative = fiboIterative(30);
console.timeEnd('Ciąg Fibonacciego przez pętlę for');

console.time('Ciąg Fibonacciego przez rekurencję ogona');
const resultRecursive = fiboTailRecursive(30);
console.timeEnd('Ciąg Fibonacciego przez rekurencję ogona');

console.log(resultRecursive == resultIterative);

})();

Stało się coś, czego się nie spodziewałem - wynik algorytmu rekurencji ogonowej był w stanie dostarczyć wynik (obliczenie 30. elementu sekwencji) prawie dwa razy szybciej niż algorytm iteracyjny w niektórych przypadkach. Nie jestem do końca pewien, czy jest to spowodowane optymalizacją rekurencji ogonowej ze strony v8, czy brakiem optymalizacji pętli for dla tej konkretnej liczby iteracji, ale wynik jest jednoznaczny - wygrywa rekurencja ogonowa.

Jest to dziwne, ponieważ zasadniczo pętla for nakłada znacznie mniej abstrakcji na działania obliczeniowe niższego poziomu i można powiedzieć, że jest bliższa podstawowej operacji komputerowej. Jednak wyniki są niezaprzeczalne - sprytnie zaprojektowana rekurencja okazuje się szybsza niż iteracja.

Używaj asynchronicznych instrukcji wywołania tak często, jak to możliwe

Ostatni akapit chciałbym poświęcić na krótkie przypomnienie metody wykonywania operacji, która również może znacząco wpłynąć na szybkość działania naszej aplikacji. Jak powinieneś wiedzieć, JavaScript to jednowątkowy język, który utrzymuje wszystkie operacje za pomocą mechanizmu pętli zdarzeń. Chodzi o cykl, który działa w kółko, a wszystkie kroki w tym cyklu dotyczą dedykowanych określonych działań.

Aby pętla była szybka i aby wszystkie cykle czekały krócej na swoją kolej, wszystkie elementy powinny być tak szybkie, jak to tylko możliwe. Unikaj wykonywania długich operacji na głównym wątku - jeśli coś trwa zbyt długo, spróbuj przenieść te obliczenia do WebWorkera lub podziel je na części, które działają asynchronicznie. Może to spowolnić niektóre operacje, ale poprawi cały ekosystem JS, w tym operacje IO, takie jak obsługa ruchu myszy lub oczekujące żądanie HTTP.

Podsumowanie

Podsumowując, jak wspomniano wcześniej, pogoń za milisekundami, które można zaoszczędzić wybierając algorytm, może w niektórych przypadkach okazać się bezsensowna. Z drugiej strony, zaniedbanie takich rzeczy w aplikacjach wymagających płynnego działania i szybkich wyników może być zabójcze dla aplikacji. W niektórych przypadkach, poza szybkością działania algorytmu, warto zadać sobie jeszcze jedno pytanie - czy abstrakcja jest obsługiwana na odpowiednim poziomie? Czy programista czytający kod będzie w stanie bez problemu go zrozumieć?

Jedynym sposobem jest zapewnienie równowagi między wydajnością, łatwością implementacji i odpowiednią abstrakcją oraz pewność, że algorytm działa poprawnie zarówno dla małych, jak i dużych ilości danych. Sposób na to jest dość prosty - bądź sprytny, rozważ różne przypadki podczas projektowania algorytmu i zaaranżuj go tak, aby zachowywał się tak wydajnie, jak to możliwe dla średnich wykonań. Wskazane jest również zaprojektowanie testów - upewnij się, że algorytm zwraca odpowiednie informacje dla różnych danych, niezależnie od tego, jak działa. Zadbaj o odpowiednie interfejsy - tak, aby zarówno dane wejściowe, jak i wyjściowe metod były czytelne, przejrzyste i dokładnie odzwierciedlały to, co robią.

Wspomniałem wcześniej, że wrócę do wiarygodności pomiaru szybkości algorytmów w powyższych przykładach. Mierzenie ich za pomocą console.time nie jest zbyt miarodajne, ale najlepiej oddaje standardowy przypadek użycia. W każdym razie poniżej przedstawiam benchmarki - niektóre z nich wyglądają nieco inaczej niż pojedyncze wykonanie ze względu na fakt, że benchmarki po prostu powtarzają daną czynność w określonym czasie i wykorzystują optymalizację v8 dla pętli.

  • https://jsben.ch/KhAqb - reduce vs for loop
  • https://jsben.ch/F4kLY - zoptymalizowane redukowanie vs pętla for
  • https://jsben.ch/MCr6g - Math.max vs pętla for
  • https://jsben.ch/A0CJB - fibo rekurencyjne vs fibo iteracyjne
  • https://jsben.ch/NFLsl - ogon rekurencyjny fibo vs iteracyjny fibo 

I to wszystko - miłego hakowania!

Czytaj więcej:

Jak ulepszyć aplikacje Vue.js? Kilka praktycznych wskazówek

Sposoby na zwiększenie wydajności Railsów

Jak napisać dobry i jakościowy kod?

Powiązane artykuły

Software Development

Tworzenie przyszłościowych aplikacji internetowych: spostrzeżenia zespołu ekspertów The Codest

Odkryj, w jaki sposób The Codest wyróżnia się w tworzeniu skalowalnych, interaktywnych aplikacji internetowych przy użyciu najnowocześniejszych technologii, zapewniając płynne doświadczenia użytkowników na wszystkich platformach. Dowiedz się, w jaki sposób nasza wiedza napędza transformację cyfrową i biznes...

THEECODEST
Software Development

10 najlepszych firm tworzących oprogramowanie na Łotwie

Dowiedz się więcej o najlepszych łotewskich firmach programistycznych i ich innowacyjnych rozwiązaniach w naszym najnowszym artykule. Odkryj, w jaki sposób ci liderzy technologiczni mogą pomóc w rozwoju Twojej firmy.

thecodest
Rozwiązania dla przedsiębiorstw i scaleupów

Podstawy tworzenia oprogramowania Java: Przewodnik po skutecznym outsourcingu

Zapoznaj się z tym niezbędnym przewodnikiem na temat skutecznego tworzenia oprogramowania Java outsourcing, aby zwiększyć wydajność, uzyskać dostęp do wiedzy specjalistycznej i osiągnąć sukces projektu z The Codest.

thecodest
Software Development

Kompletny przewodnik po outsourcingu w Polsce

Wzrost liczby outsourcing w Polsce jest napędzany przez postęp gospodarczy, edukacyjny i technologiczny, sprzyjający rozwojowi IT i przyjazny klimat dla biznesu.

TheCodest
Rozwiązania dla przedsiębiorstw i scaleupów

Kompletny przewodnik po narzędziach i technikach audytu IT

Audyty IT zapewniają bezpieczne, wydajne i zgodne z przepisami systemy. Dowiedz się więcej o ich znaczeniu, czytając cały artykuł.

The Codest
Jakub Jakubowicz CTO & Współzałożyciel

Subskrybuj naszą bazę wiedzy i bądź na bieżąco!

    O nas

    The Codest - Międzynarodowa firma programistyczna z centrami technologicznymi w Polsce.

    Wielka Brytania - siedziba główna

    • Office 303B, 182-184 High Street North E6 2JA
      Londyn, Anglia

    Polska - lokalne centra technologiczne

    • Fabryczna Office Park, Aleja
      Pokoju 18, 31-564 Kraków
    • Brain Embassy, Konstruktorska
      11, 02-673 Warszawa, Polska

      The Codest

    • Strona główna
    • O nas
    • Nasze Usługi
    • Case Studies
    • Nasze Know How
    • Kariera
    • Słownik

      Nasze Usługi

    • Konsultacje IT
    • Software Development
    • Backend Development
    • Frontend Development
    • Zespoły IT
    • Backend Dev
    • Inżynierowie rozwiązań chmurowych
    • Inżynierowie danych
    • Inne
    • Inżynierowie QA

      Raporty

    • Fakty i mity na temat współpracy z zewnętrznym partnerem programistycznym
    • Z USA do Europy: Dlaczego amerykańskie startupy decydują się na relokację do Europy?
    • Porównanie centrów rozwoju Tech Offshore: Tech Offshore Europa (Polska), ASEAN (Filipiny), Eurazja (Turcja)
    • Jakie są największe wyzwania CTO i CIO?
    • The Codest
    • The Codest
    • The Codest
    • Privacy policy
    • Warunki korzystania z witryny

    Copyright © 2025 by The Codest. Wszelkie prawa zastrzeżone.

    pl_PLPolish
    en_USEnglish de_DEGerman sv_SESwedish da_DKDanish nb_NONorwegian fiFinnish fr_FRFrench arArabic it_ITItalian jaJapanese ko_KRKorean es_ESSpanish nl_NLDutch etEstonian elGreek pl_PLPolish