ノード8を使用する時が来ましたか?

公開: 2022-03-11

ノード8が出ました! 実際、ノード8は、実際の使用法を確認するのに十分な時間使用されています。 高速な新しいV8エンジンと、async / await、HTTP / 2、asyncフックなどの新機能が付属しています。 しかし、それはあなたのプロジェクトの準備ができていますか? 確認してみましょう!

編集者注:ノード10(コードネームDubnium )もリリースされていることをご存知でしょう。 ノード8( Carbon )に焦点を当てる理由は2つあります。(1)ノード10がロングタームサポート(LTS)フェーズに入ったばかりであり、(2)ノード8がノード10よりも重要なイテレーションをマークした。

ノード8LTSのパフォーマンス

まず、この注目すべきリリースのパフォーマンスの向上と新機能を見ていきます。 改善点の1つは、NodeのJavaScriptエンジンです。

とにかく、JavaScriptエンジンとは正確には何ですか?

JavaScriptエンジンは、コードを実行して最適化します。 これは、JavaScriptをバイトコードにコンパイルする標準のインタープリターまたはジャストインタイム(JIT)コンパイラーである可能性があります。 Node.jsで使用されるJSエンジンは、すべてJITコンパイラーであり、インタープリターではありません。

V8エンジン

Node.jsは、最初からGoogleのChrome V8 JavaScriptエンジン、または単にV8を使用してきました。 一部のノードリリースは、新しいバージョンのV8と同期するために使用されます。 ただし、ここでV8バージョンを比較するときは、V8とノード8を混同しないように注意してください。

ソフトウェアのコンテキストでは、「v8」をスラングとして使用したり、「バージョン8」の正式な短縮形として使用したりすることが多いため、これは簡単に失敗します。そのため、「NodeV8」または「Node.jsV8」を「NodeJS8」と混同する場合があります。 」ですが、この記事では、物事を明確にするためにこれを避けています。V8は、Nodeのバージョンではなく、常にエンジンを意味します。

V8リリース5

ノード6は、JavaScriptエンジンとしてV8リリース5を使用します。 (ノード8の最初のいくつかのポイントリリースもV8リリース5を使用しますが、ノード6よりも新しいV8ポイントリリースを使用します。)

コンパイラ

V8リリース5以前には、2つのコンパイラがあります。

  • Full-codegenはシンプルで高速なJITコンパイラですが、遅いマシンコードを生成します。
  • クランクシャフトは、最適化されたマシンコードを生成する複雑なJITコンパイラです。
スレッド

深く掘り下げてみると、V8は複数のタイプのスレッドを使用しています。

  • メインスレッドはコードをフェッチし、コンパイルしてから実行します。
  • メインスレッドがコードを最適化している間、セカンダリスレッドはコードを実行します。
  • プロファイラースレッドは、パフォーマンスの悪いメソッドについてランタイムに通知します。 次に、クランクシャフトはこれらのメソッドを最適化します。
  • 他のスレッドはガベージコレクションを管理します。
コンパイルプロセス

まず、Full-codegenコンパイラがJavaScriptコードを実行します。 コードの実行中に、プロファイラースレッドはデータを収集して、エンジンが最適化するメソッドを決定します。 別のスレッドでは、クランクシャフトがこれらのメソッドを最適化します。

問題

上記のアプローチには2つの主な問題があります。 まず、それはアーキテクチャ的に複雑です。 第二に、コンパイルされたマシンコードははるかに多くのメモリを消費します。 消費されるメモリの量は、コードが実行される回数とは無関係です。 一度だけ実行されるコードでさえ、かなりの量のメモリを消費します。

V8リリース6

V8リリース6エンジンを使用する最初のノードバージョンはノード8.3です。

リリース6では、V8チームはこれらの問題を軽減するためにIgnitionとTurboFanを構築しました。 IgnitionとTurboFanは、それぞれFull-codegenとCrankShaftに取って代わります。

新しいアーキテクチャはより単純で、より少ないメモリを消費します。

Ignitionは、JavaScriptコードをマシンコードではなくバイトコードにコンパイルし、多くのメモリを節約します。 その後、最適化コンパイラであるTurboFanは、このバイトコードから最適化されたマシンコードを生成します。

特定のパフォーマンスの改善

以前のNodeバージョンと比較してNode8.3以降のパフォーマンスが変化した領域を見ていきましょう。

オブジェクトの作成

オブジェクトの作成は、ノード8.3以降ではノード6よりも約5倍高速です。

関数サイズ

V8エンジンは、いくつかの要因に基づいて機能を最適化する必要があるかどうかを決定します。 1つの要因は関数のサイズです。 小さな関数は最適化されますが、長い関数は最適化されません。

関数サイズはどのように計算されますか?

古いV8エンジンのクランクシャフトは、「文字数」を使用して機能サイズを決定します。 関数内の空白とコメントは、関数が最適化される可能性を減らします。 これはあなたを驚かせるかもしれませんが、当時、コメントは速度を約10%低下させる可能性があります。

Node 8.3以降では、空白やコメントなどの無関係な文字が関数のパフォーマンスに悪影響を与えることはありません。 なぜだめですか?

新しいターボファンは、関数のサイズを決定するために文字をカウントしないためです。 代わりに、抽象構文木(AST)ノードをカウントするため、実際の関数命令のみを効果的に考慮します。 Node 8.3+を使用すると、コメントと空白を好きなだけ追加できます。

Array化引数

JavaScriptの通常の関数は、暗黙のArrayのようなargumentオブジェクトを運びます。

Arrayのような意味は何ですか?

argumentsオブジェクトは、配列のように機能します。 これにはlengthプロパティがありますが、 forEachmapなどのArrayの組み込みメソッドがありません。

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");

では、 argumentsオブジェクトを配列に変換するにはどうすればよいでしょうか。 簡潔な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)は、すべてのノードバージョンのパフォーマンスを低下させます。 したがって、 forループを介してキーをコピーすると、パフォーマンスが向上します。

 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]

forループは少し面倒ですよね。 スプレッド演算子を使用することもできますが、ノード8.2以下では低速です。

 function test() { const r = [...arguments]; console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output [2, 4, 6]

ノード8.3以降では状況が変わりました。 これで、スプレッドはforループよりもはるかに高速に実行されます。

部分適用(カリー化)と結合

カリー化とは、複数の引数をとる関数を一連の関数に分解することであり、新しい関数はそれぞれ1つの引数のみを取ります。

単純なadd関数があるとしましょう。 この関数のカレーバージョンは、1つの引数num1を取ります。 別の引数num2を取り、 num1num2の合計を返す関数を返します。

 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

bindメソッドは、terser構文のカリー化された関数を返します。

 function add(num1, num2) { return num1 + num2; } const add5 = add.bind(null, 5); add5(3); // returns 8

したがってbindは信じられないほどですが、古いバージョンのノードでは遅くなります。 Node 8.3以降では、 bindははるかに高速であり、パフォーマンスへの影響を心配することなく使用できます。

実験

ノード6とノード8のパフォーマンスを高レベルで比較するために、いくつかの実験が行われました。 これらはノード8.0で実施されたため、V8リリース6のアップグレードによるノード8.3以降に固有の上記の改善は含まれていません。

ノード8のサーバーレンダリング時間はノード6より25%短縮されました。大規模なプロジェクトでは、サーバーインスタンスの数を100から75に減らすことができました。これは驚くべきことです。 ノード8で一連の500テストをテストすると、10%高速になりました。 Webpackのビルドは7%高速でした。 一般に、結果はノード8で顕著なパフォーマンスの向上を示しました。

ノード8の機能

Node 8の改善点は速度だけではありませんでした。また、いくつかの便利な新機能がもたらされました。おそらく最も重要なのは、 async/awaitです。

ノード8での非同期/待機

コールバックとプロミスは通常、JavaScriptで非同期コードを処理するために使用されます。 コールバックは、保守不可能なコードを生成することで有名です。 それらはJavaScriptコミュニティで騒乱(特にコールバック地獄として知られている)を引き起こしました。 Promisesは長い間コールバック地獄から私たちを救い出しましたが、それでも同期コードのクリーンさには欠けていました。 Async / awaitは、同期コードのように見える非同期コードを記述できる最新のアプローチです。

また、以前のNodeバージョンではasync / awaitを使用できましたが、外部ライブラリとツールが必要でした。たとえば、Babelによる追加の前処理が必要でした。 今では、箱から出してネイティブに利用できます。

async/awaitが従来のpromiseよりも優れているいくつかのケースについて説明します。

条件付き

データをフェッチしていて、ペイロードに基づいて新しいAPI呼び出しが必要かどうかを判断するとします。 以下のコードを見て、これが「従来の約束」アプローチを介してどのように行われるかを確認してください。

 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; } }); };

ご覧のとおり、上記のコードは、条件が1つ追加されているだけで、すでに乱雑に見えます。 Async / awaitには、ネストが少なくて済みます。

 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; } };

エラー処理

Async / awaitは、try/catchで同期エラーと非同期エラーの両方を処理するためのアクセスを許可します。 非同期API呼び出しからのJSONを解析するとします。 1回のtry/catchで、解析エラーとAPIエラーの両方を処理できます。

 const request = async () => { try { console.log(await getData()); } catch (err) { console.log(err); } };

中間値

約束が別の約束から解決されるべき議論を必要とする場合はどうなりますか? これは、非同期呼び出しを直列に実行する必要があることを意味します。

従来のpromiseを使用すると、次のようなコードになる可能性があります。

 const request = () => { return fetchUserData() .then(userData => { return fetchCompanyData(userData); }) .then(companyData => { return fetchRetiringPlan(userData, companyData); }) .then(retiringPlan => { const retiringPlan = retiringPlan; }); };

連鎖非同期呼び出しが必要なこの場合、Async/awaitが光ります。

 const request = async () => { const userData = await fetchUserData(); const companyData = await fetchCompanyData(userData); const retiringPlan = await fetchRetiringPlan(userData, companyData); };

並列で非同期

複数の非同期関数を並行して呼び出したい場合はどうなりますか? 以下のコードでは、 fetchHouseDataが解決されるのを待ってから、 fetchCarDataを呼び出します。 これらはそれぞれ独立していますが、順番に処理されます。 両方のAPIが解決するまで2秒待ちます。 これは良くない。

 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();

より良いアプローチは、非同期呼び出しを並行して処理することです。 以下のコードをチェックして、これが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();

これらの呼び出しを並行して処理すると、両方の呼び出しを1秒だけ待つ必要があります。

新しいコアライブラリ関数

ノード8は、いくつかの新しいコア機能ももたらします。

ファイルをコピーする

ノード8の前は、ファイルをコピーするために、2つのストリームを作成し、一方から他方にデータをパイプしていました。 以下のコードは、読み取りストリームがデータを書き込みストリームにパイプする方法を示しています。 ご覧のとおり、ファイルのコピーなどの単純なアクションではコードが乱雑になっています。

 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);

ノード8では、 fs.copyFilefs.copyFileSyncは、はるかに手間をかけずにファイルをコピーするための新しいアプローチです。

 const fs = require("fs"); fs.copyFile("firstFile.txt", "secondFile.txt", err => { if (err) { console.log(err); } else { console.log("File copied"); } });

PromisifyとCallbackify

util.promisifyは、通常の関数を非同期関数に変換します。 入力された関数は、一般的なNode.jsコールバックスタイルに従う必要があることに注意してください。 最後の引数としてコールバックを取る必要があります。つまり、 (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));

ご覧のとおり、 util.promisifyfs.readFileを非同期関数に変換しました。

一方、Node.jsにはutil.callbackifyが付属しています。 util.callbackifyutil.promisifyの反対です:非同期関数をNode.jsコールバックスタイル関数に変換します。

読み取り可能および書き込み可能の関数をdestroyします

ノード8のdestroy機能は、読み取り可能または書き込み可能なストリームを破棄/クローズ/中止するための文書化された方法です。

 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']);

上記のコードにより、 big.txtという名前の新しいファイル(まだ存在しない場合)がテキストNewtextで作成されNew text.

ノード8のReadable.destroy関数とWriteable.destroy関数は、 closeイベントとオプションのerrorイベントを発行しますdestroyは、必ずしも何かがうまくいかなかったことを意味するわけではありません。

スプレッド演算子

スプレッド演算子(別名... )はノード6で機能しましたが、配列とその他の反復可能オブジェクトでのみ機能しました。

 const arr1 = [1,2,3,4,5,6] const arr2 = [...arr1, 9] console.log(arr2) // expected output: [1,2,3,4,5,6,9]

ノード8では、オブジェクトはスプレッド演算子を使用することもできます。

 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' } */

ノード8LTSの実験的機能

実験的な機能は安定しておらず、廃止される可能性があり、時間とともに更新される可能性があります。 これらの機能は、安定するまで本番環境で使用しないでください。

非同期フック

非同期フックは、APIを介してNode内で作成された非同期リソースの存続期間を追跡します。

非同期フックを使用する前に、イベントループを理解してください。 このビデオが役立つかもしれません。 非同期フックは、非同期関数のデバッグに役立ちます。 それらにはいくつかのアプリケーションがあります。 それらの1つは、非同期関数のエラースタックトレースです。

以下のコードをご覧ください。 console.logは非同期関数であることに注意してください。 したがって、非同期フック内では使用できません。 代わりに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();

非同期フックの詳細については、このビデオをご覧ください。 特にNode.jsガイドの観点から、この記事は、例示的なアプリケーションを通じて非同期フックをわかりやすく説明するのに役立ちます。

ノード8のES6モジュール

ノード8はES6モジュールをサポートするようになり、次の構文を使用できるようになりました。

 import { UtilityService } from './utility_service';

ノード8でES6モジュールを使用するには、次のことを行う必要があります。

  1. --experimental-modulesフラグをコマンドラインに追加します
  2. ファイル拡張子の名前を.jsから.mjsに変更します

HTTP / 2

HTTP / 2は、頻繁に更新されないHTTPプロトコルの最新の更新であり、Node8.4+は実験モードでネイティブにサポートします。 その前身であるHTTP/1.1よりも高速で、安全で、効率的です。 そして、Googleはそれを使用することをお勧めします。 しかし、それは他に何をしますか?

多重化

HTTP / 1.1では、サーバーは接続ごとに一度に1つの応答しか送信できませんでした。 HTTP / 2では、サーバーは複数の応答を並行して送信できます。

サーバープッシュ

サーバーは、単一のクライアント要求に対して複数の応答をプッシュできます。 なぜこれが有益なのですか? 例としてWebアプリケーションを取り上げます。 従来、

  1. クライアントはHTMLドキュメントを要求します。
  2. クライアントは、HTMLドキュメントから必要なリソースを検出します。
  3. クライアントは、必要なリソースごとにHTTPリクエストを送信します。 たとえば、クライアントは、ドキュメントに記載されているJSおよびCSSリソースごとにHTTPリクエストを送信します。

サーバープッシュ機能は、サーバーがこれらすべてのリソースをすでに認識しているという事実を利用します。 サーバーはそれらのリソースをクライアントにプッシュします。 したがって、Webアプリケーションの例では、クライアントが最初のドキュメントを要求した後、サーバーはすべてのリソースをプッシュします。 これにより、レイテンシが短縮されます。

優先順位付け

クライアントは、優先順位付けスキームを設定して、必要な各応答の重要性を判断できます。 サーバーは、このスキームを使用して、メモリ、CPU、帯域幅、およびその他のリソースの割り当てに優先順位を付けることができます。

古い悪い習慣を取り除く

HTTP / 1.1では多重化が許可されていなかったため、低速とファイルの読み込みをカバーするために、いくつかの最適化と回避策が使用されています。 残念ながら、これらの手法はRAM消費量の増加とレンダリングの遅延を引き起こします。

  • ドメインシャーディング:接続が分散されて並行して処理されるように、複数のサブドメインが使用されました。
  • CSSファイルとJavaScriptファイルを組み合わせてリクエスト数を減らします。
  • スプライトマップ:画像ファイルを組み合わせてHTTPリクエストを減らします。
  • インライン化:CSSとJavaScriptは、接続数を減らすためにHTMLに直接配置されます。

HTTP / 2を使用すると、これらの手法を忘れて、コードに集中できます。

しかし、HTTP / 2をどのように使用しますか?

ほとんどのブラウザは、セキュリティで保護されたSSL接続を介してのみHTTP/2をサポートします。 この記事は、自己署名証明書の構成に役立ちます。 生成された.crtファイルと.keyファイルをsslというディレクトリに追加します。 次に、以下のコードをserver.jsという名前のファイルに追加します。

この機能を有効にするには、コマンドラインで--expose-http2フラグを使用することを忘れないでください。 つまり、この例の実行コマンドは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}`) );

もちろん、ノード8、ノード9、ノード10などは引き続き古いHTTP 1.1をサポートしています。標準のHTTPトランザクションに関する公式のNode.jsドキュメントは、長期間古くなることはありません。 ただし、HTTP / 2を使用する場合は、このNode.jsガイドを使用してさらに詳しく知ることができます。

それで、最後にNode.js 8を使用する必要がありますか?

ノード8には、パフォーマンスが向上し、async / await、HTTP/2などの新機能が追加されました。 エンドツーエンドの実験では、ノード8はノード6よりも約25%高速であることが示されています。これにより、大幅なコスト削減につながります。 したがって、グリーンフィールドプロジェクトの場合、絶対に! しかし、既存のプロジェクトの場合、Nodeを更新する必要がありますか?

これは、既存のコードの多くを変更する必要があるかどうかによって異なります。 このドキュメントには、ノード6からの場合の、すべてのノード8の重大な変更がリストされています。最新のノード8バージョンを使用してプロジェクトのすべてのnpmパッケージを再インストールすることにより、一般的な問題を回避することを忘れないでください。 また、開発マシンでは、本番サーバーと同じNode.jsバージョンを常に使用してください。 頑張ってください!

関連している:
  • なぜ地獄はNode.jsを使用するのでしょうか? ケースバイケースのチュートリアル
  • Node.jsアプリケーションでのメモリリークのデバッグ
  • Node.jsでのセキュアRESTAPIの作成
  • キャビンフィーバーコーディング:Node.jsバックエンドチュートリアル