Czy nadszedł czas, aby użyć węzła 8?
Opublikowany: 2022-03-11Węzeł 8 jest niedostępny! W rzeczywistości Node 8 był już wystarczająco długo niedostępny, aby zobaczyć solidne wykorzystanie w świecie rzeczywistym. Został wyposażony w nowy, szybki silnik V8 i nowe funkcje, w tym async/await, HTTP/2 i async hooks. Ale czy jest gotowy na Twój projekt? Dowiedzmy Się!
Uwaga redaktora: Prawdopodobnie wiesz, że Node 10 (o nazwie kodowej Dubnium ) również jest niedostępny. Decydujemy się skupić na węźle 8 ( węgiel ) z dwóch powodów: (1) Węzeł 10 właśnie wchodzi w fazę długoterminowego wsparcia (LTS), oraz (2) Węzeł 8 zaznaczył bardziej znaczącą iterację niż Węzeł 10 .
Wydajność w węźle 8 LTS
Zaczniemy od przyjrzenia się ulepszeniom wydajności i nowym funkcjom tej niezwykłej wersji. Jednym z głównych obszarów ulepszeń jest silnik JavaScript Node.
Czym właściwie jest silnik JavaScript?
Silnik JavaScript wykonuje i optymalizuje kod. Może to być standardowy interpreter lub kompilator just-in-time (JIT), który kompiluje JavaScript do kodu bajtowego. Silniki JS używane przez Node.js są kompilatorami JIT, a nie interpreterami.
Silnik V8
Node.js od samego początku korzystał z silnika Google Chrome V8 JavaScript , lub po prostu V8 . Niektóre wydania Node są używane do synchronizacji z nowszą wersją V8. Uważaj jednak, aby nie pomylić V8 z Node 8, ponieważ porównujemy tutaj wersje V8.
Łatwo się o to potknąć, ponieważ w kontekście oprogramowania często używamy „v8” jako slangu lub nawet oficjalnej krótkiej formy dla „wersji 8”, więc niektórzy mogą łączyć „Node V8” lub „Node.js V8” z „NodeJS 8”. ”, ale unikaliśmy tego w tym artykule, aby wszystko było jasne: V8 zawsze będzie oznaczać silnik, a nie wersję Node.
Wersja V8 5
Węzeł 6 używa V8 w wersji 5 jako swojego silnika JavaScript. (Pierwsze kilka wydań punktowych węzła 8 również korzysta z wersji V8 5, ale używają nowszej wersji punktowej V8 niż w przypadku węzła 6.)
Kompilatory
Wersje V8 5 i wcześniejsze mają dwa kompilatory:
- Full-codegen to prosty i szybki kompilator JIT, ale generuje wolny kod maszynowy.
- Wał korbowy to złożony kompilator JIT, który generuje zoptymalizowany kod maszynowy.
Wątki
W głębi duszy V8 wykorzystuje więcej niż jeden rodzaj wątku:
- Główny wątek pobiera kod, kompiluje go, a następnie wykonuje.
- Wątki pomocnicze wykonują kod, podczas gdy wątek główny optymalizuje kod.
- Wątek profilera informuje środowisko uruchomieniowe o nieefektywnych metodach. Wał korbowy następnie optymalizuje te metody.
- Inne wątki zarządzają wyrzucaniem śmieci.
Proces kompilacji
Najpierw kompilator Full-codegen wykonuje kod JavaScript. Podczas wykonywania kodu wątek profilera zbiera dane, aby określić, które metody zoptymalizuje aparat. Na innym wątku wał korbowy optymalizuje te metody.
Zagadnienia
Powyższe podejście wiąże się z dwoma głównymi problemami. Po pierwsze, jest złożony architektonicznie. Po drugie, skompilowany kod maszynowy zużywa znacznie więcej pamięci. Ilość zużytej pamięci jest niezależna od tego, ile razy kod jest wykonywany. Nawet kod, który uruchamia się tylko raz, również zajmuje znaczną ilość pamięci.
Wersja V8 6
Pierwszą wersją Node korzystającą z silnika V8 w wersji 6 jest Node 8.3.
W wersji 6 zespół V8 zbudował Ignition i TurboFan, aby złagodzić te problemy. Zapłon i TurboFan zastępują odpowiednio Full-codegen i CrankShaft.
Nowa architektura jest prostsza i zużywa mniej pamięci.
Ignition kompiluje kod JavaScript do kodu bajtowego zamiast kodu maszynowego, oszczędzając dużo pamięci. Następnie TurboFan, kompilator optymalizujący, generuje zoptymalizowany kod maszynowy z tego kodu bajtowego.
Konkretne ulepszenia wydajności
Przejdźmy przez obszary, w których wydajność w Node 8.3+ zmieniła się w stosunku do starszych wersji Node.
Tworzenie obiektów
Tworzenie obiektów jest około pięć razy szybsze w węźle 8.3+ niż w węźle 6.
Rozmiar funkcji
Silnik V8 decyduje o optymalizacji funkcji na podstawie kilku czynników. Jednym z czynników jest rozmiar funkcji. Małe funkcje są zoptymalizowane, a długie nie.
Jak obliczany jest rozmiar funkcji?
Wał korbowy w starym silniku V8 wykorzystuje „liczbę znaków” do określenia rozmiaru funkcji. Białe znaki i komentarze w funkcji zmniejszają szanse na jej optymalizację. Wiem, że może cię to zaskoczyć, ale wtedy komentarz mógł zmniejszyć prędkość o około 10%.
W węźle 8.3+ nieistotne znaki, takie jak białe znaki i komentarze, nie wpływają negatywnie na wydajność funkcji. Dlaczego nie?
Ponieważ nowy TurboFan nie liczy znaków w celu określenia rozmiaru funkcji. Zamiast tego zlicza węzły abstrakcyjnego drzewa składni (AST), więc efektywnie uwzględnia tylko rzeczywiste instrukcje funkcyjne . Używając Node 8.3+, możesz dodawać komentarze i odstępy tyle, ile chcesz.
Argumenty określające Array
Regularne funkcje w JavaScript przenoszą niejawny obiekt argument
podobny do Array
.
Co oznacza Array
przypominająca?
Obiekt arguments
działa trochę jak tablica. Ma właściwość length
, ale brakuje mu wbudowanych metod Array
, takich jak forEach
i map
.
Oto jak działa obiekt arguments
:
function foo() { console.log(arguments[0]); // Expected output: a console.log(arguments[1]); // Expected output: b console.log(arguments[2]); // Expected output: c } foo("a", "b", "c");
Jak więc przekonwertować obiekt arguments
na tablicę? Za pomocą zwięzłego Array.prototype.slice.call(arguments)
.
function test() { const r = Array.prototype.slice.call(arguments); console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output: [2, 4, 6]
Array.prototype.slice.call(arguments)
pogarsza wydajność we wszystkich wersjach węzła. Dlatego kopiowanie kluczy za pomocą pętli for
działa lepiej:
function test() { const r = []; for (index in arguments) { r.push(arguments[index]); } console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output [2, 4, 6]
Pętla for
jest trochę kłopotliwa, prawda? Moglibyśmy użyć operatora rozproszenia, ale działa on wolno w węźle 8.2 i niższych:
function test() { const r = [...arguments]; console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output [2, 4, 6]
Sytuacja zmieniła się w węźle 8.3+. Teraz spread wykonuje się znacznie szybciej, nawet szybciej niż pętla for.
Częściowe zastosowanie (Currying) i wiązanie
Currying to podział funkcji, która przyjmuje wiele argumentów na serię funkcji, w których każda nowa funkcja przyjmuje tylko jeden argument.
Powiedzmy, że mamy prostą funkcję add
. Curried wersja tej funkcji przyjmuje jeden argument, num1
. Zwraca funkcję, która przyjmuje inny argument num2
i zwraca sumę num1
i num2
:
function add(num1, num2) { return num1 + num2; } add(4, 6); // returns 10 function curriedAdd(num1) { return function(num2) { return num1 + num2; }; } const add5 = curriedAdd(5); add5(3); // returns 8
Metoda bind
zwraca funkcję curried o zwięzłej składni.
function add(num1, num2) { return num1 + num2; } const add5 = add.bind(null, 5); add5(3); // returns 8
Zatem bind
jest niesamowity, ale jest powolny w starszych wersjach Node. W Node 8.3+ bind
jest znacznie szybszy i możesz go używać bez martwienia się o jakiekolwiek spadki wydajności.
Eksperymenty
Przeprowadzono kilka eksperymentów w celu porównania wydajności węzła 6 z węzłem 8 na wysokim poziomie. Zwróć uwagę, że zostały one przeprowadzone w Node 8.0, więc nie zawierają wymienionych powyżej ulepszeń, które są specyficzne dla Node 8.3+ dzięki aktualizacji V8 w wersji 6.
Czas renderowania serwera w węźle 8 był o 25% krótszy niż w węźle 6. W dużych projektach liczbę instancji serwera można było zmniejszyć ze 100 do 75. To zdumiewające. Testowanie zestawu 500 testów w węźle 8 było o 10% szybsze. Kompilacje Webpack były o 7% szybsze. Ogólnie wyniki wykazały zauważalny wzrost wydajności w węźle 8.
Funkcje węzła 8
Szybkość nie była jedynym ulepszeniem w Node 8. Przyniosła także kilka przydatnych nowych funkcji — być może najważniejsze, async/await .
Asynchronizacja/oczekiwanie w węźle 8
Wywołania zwrotne i obietnice są zwykle używane do obsługi kodu asynchronicznego w JavaScript. Wywołania zwrotne są znane z tworzenia kodu, którego nie można konserwować. Spowodowali chaos (znany konkretnie jako callback hell ) w społeczności JavaScript. Obietnice na długi czas ratowały nas z piekła wywołań zwrotnych, ale nadal brakowało im czystości kodu synchronicznego. Async/await to nowoczesne podejście, które umożliwia pisanie kodu asynchronicznego, który wygląda jak kod synchroniczny.
I chociaż async/await mógł być używany w poprzednich wersjach Node, wymagał zewnętrznych bibliotek i narzędzi — na przykład dodatkowego przetwarzania wstępnego przez Babel. Teraz jest dostępny natywnie, po wyjęciu z pudełka.
Opowiem o niektórych przypadkach, w których async/await jest lepszy od konwencjonalnych obietnic.
Warunkowe
Wyobraź sobie, że pobierasz dane i na podstawie ładunku określisz, czy potrzebne jest nowe wywołanie interfejsu API. Spójrz na poniższy kod, aby zobaczyć, jak odbywa się to za pomocą podejścia „konwencjonalnych obietnic”.
const request = () => { return getData().then(data => { if (!data.car) { return fetchForCar(data.id).then(carData => { console.log(carData); return carData; }); } else { console.log(data); return data; } }); };
Jak widać, powyższy kod już wygląda na niechlujny, tylko z jednego dodatkowego warunku. Async/await obejmuje mniej zagnieżdżania:
const request = async () => { const data = await getData(); if (!data.car) { const carData = await fetchForCar(data); console.log(carData); return carData; } else { console.log(data); return data; } };
Obsługa błędów
Async/await zapewnia dostęp do obsługi błędów synchronicznych i asynchronicznych w try/catch. Załóżmy, że chcesz przeanalizować kod JSON pochodzący z asynchronicznego wywołania interfejsu API. Pojedyncza próba/złapanie może obsłużyć zarówno błędy analizy, jak i błędy API.
const request = async () => { try { console.log(await getData()); } catch (err) { console.log(err); } };
Wartości pośrednie
A co, jeśli obietnica wymaga argumentu, który należy rozstrzygnąć na podstawie innej obietnicy? Oznacza to, że wywołania asynchroniczne muszą być wykonywane szeregowo.
Używając konwencjonalnych obietnic, możesz skończyć z takim kodem:
const request = () => { return fetchUserData() .then(userData => { return fetchCompanyData(userData); }) .then(companyData => { return fetchRetiringPlan(userData, companyData); }) .then(retiringPlan => { const retiringPlan = retiringPlan; }); };
Async/await świeci w tym przypadku, gdy potrzebne są połączone wywołania asynchroniczne:
const request = async () => { const userData = await fetchUserData(); const companyData = await fetchCompanyData(userData); const retiringPlan = await fetchRetiringPlan(userData, companyData); };
Asynchroniczna równoległa
Co zrobić, jeśli chcesz równolegle wywołać więcej niż jedną funkcję asynchroniczną? W poniższym kodzie poczekamy na rozwiązanie fetchHouseData
, a następnie fetchCarData
. Chociaż każdy z nich jest niezależny od drugiego, są przetwarzane sekwencyjnie. Poczekaj dwie sekundy na rozwiązanie obu interfejsów API. To nie jest dobre.

function fetchHouseData() { return new Promise(resolve => setTimeout(() => resolve("Mansion"), 1000)); } function fetchCarData() { return new Promise(resolve => setTimeout(() => resolve("Ferrari"), 1000)); } async function action() { const house = await fetchHouseData(); // Wait one second const car = await fetchCarData(); // ...then wait another second. console.log(house, car, " in series"); } action();
Lepszym podejściem jest równoległe przetwarzanie wywołań asynchronicznych. Sprawdź poniższy kod, aby zorientować się, jak jest to osiągane w async/await.
async function parallel() { houseDataPromise = fetchHouseData(); carDataPromise = fetchCarData(); const house = await houseDataPromise; // Wait one second for both const car = await carDataPromise; console.log(house, car, " in parallel"); } parallel();
Równoczesne przetwarzanie tych wywołań wymaga odczekania tylko jednej sekundy na oba wywołania.
Nowe podstawowe funkcje biblioteki
Węzeł 8 oferuje również kilka nowych podstawowych funkcji.
Kopiuj pliki
Przed węzłem 8, aby skopiować pliki, tworzyliśmy dwa strumienie i dane potoku z jednego do drugiego. Poniższy kod pokazuje, w jaki sposób strumień odczytu przesyła dane do strumienia zapisu. Jak widać, kod jest zagracony przy tak prostej czynności, jak kopiowanie pliku.
const fs = require('fs'); const rd = fs.createReadStream('sourceFile.txt'); rd.on('error', err => { console.log(err); }); const wr = fs.createWriteStream('target.txt'); wr.on('error', err => { console.log(err); }); wr.on('close', function(ex) { console.log('File Copied'); }); rd.pipe(wr);
W węźle 8 fs.copyFile
i fs.copyFileSync
to nowe podejścia do kopiowania plików przy znacznie mniejszym wysiłku.
const fs = require("fs"); fs.copyFile("firstFile.txt", "secondFile.txt", err => { if (err) { console.log(err); } else { console.log("File copied"); } });
Obiecaj i oddzwoń
util.promisify
konwertuje zwykłą funkcję na funkcję asynchroniczną. Zauważ, że wpisana funkcja powinna być zgodna ze wspólnym stylem wywołania zwrotnego Node.js. Powinien przyjąć wywołanie zwrotne jako ostatni argument, tj. (error, payload) => { ... }
.
const { promisify } = require('util'); const fs = require('fs'); const readFilePromisified = promisify(fs.readFile); const file_path = process.argv[2]; readFilePromisified(file_path) .then((text) => console.log(text)) .catch((err) => console.log(err));
Jak widać, util.promisify
przekonwertował fs.readFile
na funkcję asynchroniczną.
Z drugiej strony Node.js zawiera util.callbackify
. util.callbackify
jest przeciwieństwem util.promisify
: konwertuje funkcję asynchroniczną na funkcję stylu wywołania zwrotnego Node.js.
destroy
funkcję dla obiektów do odczytu i zapisu
Funkcja destroy
w węźle 8 to udokumentowany sposób na zniszczenie/zamknięcie/przerwanie strumienia do odczytu lub zapisu:
const fs = require('fs'); const file = fs.createWriteStream('./big.txt'); file.on('error', errors => { console.log(errors); }); file.write(`New text.\n`); file.destroy(['First Error', 'Second Error']);
Powyższy kod powoduje utworzenie nowego pliku o nazwie big.txt
(jeśli jeszcze nie istnieje) z tekstem New text.
.
Funkcje Readable.destroy
i Writeable.destroy
w węźle 8 emitują zdarzenie close
i opcjonalne zdarzenie error
— destroy
niekoniecznie oznacza, że coś poszło nie tak.
Operator rozprzestrzeniania się
Operator rozproszenia (aka ...
) działał w węźle 6, ale tylko z tablicami i innymi iteracjami:
const arr1 = [1,2,3,4,5,6] const arr2 = [...arr1, 9] console.log(arr2) // expected output: [1,2,3,4,5,6,9]
W węźle 8 obiekty mogą również używać operatora rozsunięcia:
const userCarData = { type: 'ferrari', color: 'red' }; const userSettingsData = { lastLoggedIn: '12/03/2019', featuresPlan: 'premium' }; const userData = { ...userCarData, name: 'Youssef', ...userSettingsData }; console.log(userData); /* Expected output: { type: 'ferrari', color: 'red', name: 'Youssef', lastLoggedIn: '12/03/2019', featuresPlan: 'premium' } */
Funkcje eksperymentalne w Node 8 LTS
Funkcje eksperymentalne nie są stabilne, mogą stać się przestarzałe i z czasem mogą zostać zaktualizowane. Nie używaj żadnej z tych funkcji w środowisku produkcyjnym , dopóki nie ustabilizują się.
Haki asynchroniczne
Haki asynchroniczne śledzą okres istnienia zasobów asynchronicznych utworzonych w węźle za pośrednictwem interfejsu API.
Upewnij się, że rozumiesz pętlę zdarzeń, zanim przejdziesz dalej z hakami asynchronicznymi. Ten film może pomóc. Haki asynchroniczne są przydatne do debugowania funkcji asynchronicznych. Mają kilka zastosowań; jednym z nich są ślady stosu błędów dla funkcji asynchronicznych.
Spójrz na poniższy kod. Zauważ, że console.log
jest funkcją asynchroniczną. Dlatego nie można go używać w hakach asynchronicznych. Zamiast tego używany jest fs.writeSync
.
const asyncHooks = require('async_hooks'); const fs = require('fs'); const init = (asyncId, type, triggerId) => fs.writeSync(1, `${type} \n`); const asyncHook = asyncHooks.createHook({ init }); asyncHook.enable();
Obejrzyj ten film, aby dowiedzieć się więcej o hakach asynchronicznych. Jeśli chodzi konkretnie o przewodnik Node.js, ten artykuł pomaga wyjaśnić asynchroniczne haki za pomocą przykładowej aplikacji.
Moduły ES6 w węźle 8
Węzeł 8 obsługuje teraz moduły ES6, umożliwiając korzystanie z następującej składni:
import { UtilityService } from './utility_service';
Aby użyć modułów ES6 w węźle 8, musisz wykonać następujące czynności.
- Dodaj
--experimental-modules
do wiersza poleceń - Zmień nazwy rozszerzeń plików z
.js
na.mjs
HTTP/2
HTTP/2 to najnowsza aktualizacja niezbyt często aktualizowanego protokołu HTTP, a Node 8.4+ obsługuje go natywnie w trybie eksperymentalnym. Jest szybszy, bezpieczniejszy i bardziej wydajny niż jego poprzednik, HTTP/1.1. A Google zaleca, abyś go używał. Ale co jeszcze robi?
Multipleksowanie
W HTTP/1.1 serwer mógł wysłać tylko jedną odpowiedź na połączenie naraz. W HTTP/2 serwer może wysyłać równolegle więcej niż jedną odpowiedź.
Serwer Push
Serwer może przesyłać wiele odpowiedzi na jedno żądanie klienta. Dlaczego jest to korzystne? Weźmy na przykład aplikację internetową. Umownie,
- Klient żąda dokumentu HTML.
- Klient odnajduje potrzebne zasoby z dokumentu HTML.
- Klient wysyła żądanie HTTP dla każdego wymaganego zasobu. Na przykład klient wysyła żądanie HTTP dla każdego zasobu JS i CSS wymienionego w dokumencie.
Funkcja server-push wykorzystuje fakt, że serwer już wie o wszystkich tych zasobach. Serwer przekazuje te zasoby do klienta. Tak więc dla przykładu aplikacji internetowej serwer wysyła wszystkie zasoby po tym, jak klient zażąda dokumentu początkowego. Zmniejsza to opóźnienie.
Priorytetyzacja
Klient może ustawić schemat priorytetów, aby określić, jak ważna jest każda wymagana odpowiedź. Serwer może następnie użyć tego schematu do ustalenia priorytetów alokacji pamięci, procesora, przepustowości i innych zasobów.
Porzucanie starych złych nawyków
Ponieważ protokół HTTP/1.1 nie pozwalał na multipleksowanie, zastosowano kilka optymalizacji i obejść, aby ukryć niską prędkość i ładowanie plików. Niestety te techniki powodują wzrost zużycia pamięci RAM i opóźnione renderowanie:
- Dzielenie domeny na fragmenty: użyto wielu poddomen, aby połączenia były rozproszone i przetwarzane równolegle.
- Łączenie plików CSS i JavaScript w celu zmniejszenia liczby żądań.
- Mapy sprite: łączenie plików graficznych w celu zmniejszenia liczby żądań HTTP.
- Inlining: CSS i JavaScript są umieszczane bezpośrednio w kodzie HTML, aby zmniejszyć liczbę połączeń.
Teraz dzięki HTTP/2 możesz zapomnieć o tych technikach i skupić się na swoim kodzie.
Ale jak korzystać z HTTP/2?
Większość przeglądarek obsługuje protokół HTTP/2 tylko za pośrednictwem bezpiecznego połączenia SSL. Ten artykuł może pomóc w skonfigurowaniu certyfikatu z podpisem własnym. Dodaj wygenerowany plik .crt
i plik .key
do katalogu o nazwie ssl
. Następnie dodaj poniższy kod do pliku o nazwie server.js
.
Pamiętaj, aby użyć flagi --expose-http2
w wierszu poleceń, aby włączyć tę funkcję. Na przykład polecenie uruchomienia dla naszego przykładu to node server.js --expose-http2
.
const http2 = require('http2'); const path = require('path'); const fs = require('fs'); const PORT = 3000; const secureServerOptions = { cert: fs.readFileSync(path.join(__dirname, './ssl/server.crt')), key: fs.readFileSync(path.join(__dirname, './ssl/server.key')) }; const server = http2.createSecureServer(secureServerOptions, (req, res) => { res.statusCode = 200; res.end('Hello from Toptal'); }); server.listen( PORT, err => err ? console.error(err) : console.log(`Server listening to port ${PORT}`) );
Oczywiście Węzeł 8, Węzeł 9, Węzeł 10 itd. nadal obsługują stary HTTP 1.1 — oficjalna dokumentacja Node.js dotycząca standardowej transakcji HTTP nie będzie przestarzała przez długi czas. Ale jeśli chcesz używać HTTP/2, możesz zagłębić się w ten przewodnik Node.js.
Czy powinienem więc używać Node.js 8 na końcu?
Węzeł 8 pojawił się z ulepszeniami wydajności i nowymi funkcjami, takimi jak async/await, HTTP/2 i inne. Kompleksowe eksperymenty wykazały, że Węzeł 8 jest o około 25% szybszy niż Węzeł 6. Prowadzi to do znacznych oszczędności kosztów. A więc dla projektów typu greenfield, jak najbardziej! Ale czy w przypadku istniejących projektów należy zaktualizować Node?
Zależy to od tego, czy musiałbyś zmienić znaczną część istniejącego kodu. Ten dokument zawiera listę wszystkich zmian wprowadzających w Node 8, jeśli pochodzisz z Node 6. Pamiętaj, aby uniknąć typowych problemów, ponownie instalując wszystkie pakiety npm
projektu przy użyciu najnowszej wersji Node 8. Ponadto zawsze używaj tej samej wersji Node.js na komputerach deweloperskich, co na serwerach produkcyjnych. Powodzenia!
- Dlaczego do diabła miałbym używać Node.js? Samouczek dla każdego przypadku
- Debugowanie wycieków pamięci w aplikacjach Node.js
- Tworzenie bezpiecznego interfejsu API REST w Node.js
- Kodowanie Cabin Fever: samouczek dotyczący back-endu Node.js