はじめに
こんにちは。新卒エンジニアの お涼 です。
エンジニアリングの世界に飛び込んだばかりの私ですが、入社して早々、こんなことがありました。
Node…ノード…?
聞いたことはあるものの、Node.jsが一体何なのかはさっぱり分かりませんでした。しかも分からないまましばらく使い、npm installなどもしておりました。しかし研修期間が終わり、Node.jsを使用したWebアプリケーションの開発案件にアサインされ、分からないでは済まされない時がやってきました。
そのアプリケーションでは、画面上の複数箇所で何時間もアニメーションを描画し続ける必要がありました。私はとりあえず動くものをと、複数のコンポーネントでアニメーションループを乱用しました。その結果、アニメーションは1分と経たずカクカクし始め、PCはコンビニの肉まんを包み紙越しに握った時くらい熱くなりました(ファンレス)。
検証ツールで原因を探っていくと、どうやら無駄な再レンダリングの嵐によりアニメーションループの関数が呼ばれまくっており、パフォーマンスを圧迫していました。Webアプリがなぜこうした挙動をするのか?原因を深掘りしていくうちに、Node.jsという技術の存在を改めて理解する必要性が生じました。
ということで、「Node.jsってなんぞや?」 というところから始まり、「なんとなく分かった気がする」状態になるまでの私の試行錯誤ストーリーをお届けします。
エンジニアの成長初期は、「分からない → 質問する or 自分で調べる → さらにその答えが疑問を生む → ある日急に部分的に理解が進んでいく(でも完全じゃない)」…というサイクルをずっと繰り返すと思っています。新卒でエンジニアを始めた私は、このサイクルに慣れるまで大変でした。
だからこそ、このブログを通じて、同じように悩んでいる方を励ませたらと思っています。そして何より、「自分以外にも分からなかった人がいるんだな」と安心してもらえたら、私のNode.jsに対する試行錯誤も報われる気がします。
Node.jsって?
さて、普段何気なく「Nodeでこれ動かして〜」とか言われたり、なんなら言ったりもしてますが、正直なところ概要を説明しろって言われたら「…一旦ググっていいですか」状態です。
そこで、頼りになる公式ドキュメントを開いてみました。…ええ、上司であるKさんの「まずは質問する前にドキュメントに目を通しておくといいよ」という温かいアドバイスにしたがって、です。ありがたい。
ちなみにKさんは、弊社が誇る凄腕フルスタックエンジニア。そのフルスタック具合と的確すぎるアドバイスには定評があり、「Kさんの脳はマルチコア」とまで言われています。もう社内では伝説です。
公式ドキュメントの一節:
Node.js is similar in design to, and influenced by, systems like Ruby’s Event Machine and Python’s Twisted. Node.js takes the event model a bit further. It presents an event loop as a runtime construct instead of as a library. In other systems, there is always a blocking call to start the event-loop. Typically, behavior is defined through callbacks at the beginning of a script, and at the end a server is started through a blocking call like EventMachine::run(). In Node.js, there is no such start-the-event-loop call. Node.js simply enters the event loop after executing the input script. Node.js exits the event loop when there are no more callbacks to perform. This behavior is like browser JavaScript — the event loop is hidden from the user.
Furthermore, users of Node.js are free from worries of dead-locking the process, since there are no locks. Almost no function in Node.js directly performs I/O, so the process never blocks except when the I/O is performed using synchronous methods of Node.js standard library. Because nothing blocks, scalable systems are very reasonable to develop in Node.js.
…えっと、何やらむずかしいこと書いてますけど、私が10回くらい読んでわかった要点は3つです。
- イベントループがいい感じらしい。
- ノンブロッキングI/Oだったらなんか処理が止まらないらしい。
- シングルスレッドで並行処理。並行なのに1本らしい。
ここで出てくる小難しいワード「ノンブロッキングI/O」「イベントループ」。初めて聞いた私の頭は混乱を極め、ドキュメントを読みながら一人唸っていると、スッと私に携帯の画面を差し出すKさん。
これいい本だよー🤳(電子書籍の画面を見せてくださっている)
その本のタイトルは「O’Reilly Japan – ハンズオンNode.js」。
早速Amazonでポチり、翌日に届きました(プライム会員)。期待を胸にページをめくったものの、そこにも何やら新しいワードが連発し、私の頭はさらに混乱してしまいます。
毎日、通勤電車で吊革を片手にこの本と格闘し、オフィスではKさんに質問をしつつ、返ってくる答えがまた新しい疑問を生むという無限ループに陥る日々。
でも、ある日を境に「分かった気がする」という瞬間がちらほら訪れるようになったのです。感動。ということで以下に、Node.jsについて分かったことを簡単な言葉でまとめてみます。
渋滞知らずのNode.js
まずはI/Oの話から始めます。I/Oは Input/Output の略で、データベースやファイルシステムなどのアプリケーションの外との「やり取り」を指します。このやり取りには、案外時間がかかります。例えば、「データを送信する」「データを受信する」といった処理中、アプリケーションがほかの作業を止めて待つことになります。この「待ち」の状態をブロッキングと呼びます。
さらに問題を深刻化させるのが、JavaScriptがシングルスレッドだということです。シングルスレッドは一車線しかない道路のようなもので、先頭の車がI/Oで停止すると、後続の車両すべてが足止めされます。渋滞が発生するわけです。
そんな渋滞を解消するのが、Node.jsのノンブロッキングI/Oとイベントループです。これは「停止している間に別のタスクをこなす仕組み」です。ノンブロッキングI/Oを使えば、停止中の車(I/O処理)が道の端に寄り、後続の車(他の処理)がスイスイ通れるようになります。
この、一車線にも関わらずまるで複数車線があるかのような挙動を可能にしているのが、Node.jsのイベントループという魔法なのです。イベントループのおかげで、渋滞中でも待機時間を有効活用し、道路を効率よく流れるようにしてくれます。
そう、Node.jsは、渋滞に強い「できるヤツ」として評価されています。特にWebサーバーやリアルタイムアプリケーションのように待ち時間が多くなりがちな処理にはもってこいな訳です。ただし、この恩恵をフルに受けるには、私たちエンジニアが非同期処理の流れを理解し、さらに使いこなす必要があります。…ええ、世の中はそう簡単にはいきません。
「どうしても寄らないやつ(ブロッキングI/O)」の存在
さらに公式ドキュメントを読み進めると、片隅に地味ながら重要な一文を見つけました。
公式ドキュメントの一節:
All of the I/O methods in the Node.js standard library provide asynchronous versions, which are non-blocking, and accept callback functions. Some methods also have blocking counterparts, which have names that end with Sync.
「Node.js標準のI/Oメソッドはノンブロッキングで提供されています。ただし、一部には”Sync”で終わるブロッキングI/Oバージョンも用意されています。」と書いてあります。
つまり、Node.jsのI/Oメソッドは基本的に「端っこに寄ってくれる車」が標準ですが、「どうしても寄らないやつ(ブロッキングI/O)」もいるという話です。寄らないやつは関数名に “Sync” とついているので区別ができるようになっているみたいです。例えば、「fs.readFileSync」なんてやつですね。
さて、私はこれを読んである疑問にぶつかりました。
なぜSync関数は存在するのか?
最初、私はこう考えました。
「Sync関数は、決まった順番でI/O処理を実行するために用意されているのでは?」
例えば、次のような処理を考えてみてください。
ファイルの存在を確認する→ファイルを読み込む→ファイルを削除する
このように処理の順序に意味があるケースでは、非同期処理特有の挙動が問題を引き起こすことがあります。道路で渋滞中の車を追い越すように、タスクの処理順序が前後する可能性があるからです。
もし処理が順番通りに実行されなければ、ファイルを読み込む前に削除してしまうといった、意図しない結果を招くことも…。
でも待てよ…
ここで、冷静に思い出しました。
非同期処理には async/await という強力なツールが用意されているではないか、と。async/await を使えば非同期処理でも順番を制御できるため、処理の順序を保証するのはSync関数でなくても可能です。となると、ますます疑問が深まります。
なんでわざわざ 「どうしても寄らないやつ」を用意したの…?
疑問をKさんにぶつけてみた
誰がNodeでわざわざSync関数使うんですか?
前はトップレベルawaitが使えなかったらねー
トップレベルawait…?(また新しいワード登場した)
早速私は、トップレベル await なるものについて調べてみると、このドキュメントにたどり着きました。
内容を読んでみると、言っていることは何となく分かります。たしかに便利そうです。特に、モジュールの初期化やデータの事前取得といった処理には必要そうな雰囲気を感じる。でも、Syncメソッドが必要な場面が、まだピンとこない…。
静まりかえった月曜日のオフィスで考え続け、最終的に一つの結論に辿り着きました。
そもそも、駆け出しエンジニアの私は「トップレベル await が必要な場面」にまだ出会ったことがないんだからイメージできなくて当たり前では?(盲点)
そう、「モジュール初期化時に非同期処理が必要」なんて高尚な状況、たぶんまだ私の実務には関係ない。それなのに「なんで理解できないんだろう…」と悩む必要なんてないんです。ということで、このテーマは「未来の自分への宿題」とさせてください。きっと経験を積んだ頃に「あの時は何も分かってなかったな」と思いながら、この記事を見返すことでしょう…。
さいごに
次回の記事では、このSync関数の謎についてお届けできる……といいなと思っています。
ここまで試行錯誤を続けられたのも、Kさんの適切なアドバイスと忍耐強いサポートのおかげです。私の初歩的な質問にも一つひとつ丁寧に答えてくださり、背中を押してくれたことに心から感謝しています。
この調子だと、「もうちょっと自分で考えようか?」と言われてしまいそうなので(すでに結構言われている)、次に質問する時は少しでも成長した自分を見せられるよう、もっともっと頑張ります。
そして、この記事を読んでくださったすべての方に「O’Reilly Japan – ハンズオンNode.js」をお勧めします。初心者には難しいですが、何度読んでも気づきがあります。
それでは、また次の記事でお会いしましょう。ありがとうございました👋
Nodeでこれ動かして、〜〜して、〜〜してね。