JavaScript Registryの今後

さて、前回は tinkyarn v2 における CLI 戦略の話でした。次は JavaScript Registry についてです。

ちなみにこの内容が今回 JSConf.EU 2019 で一番盛り上がったトピックです。

JavaScript Registry とは

JavaScript Package をバックエンドで管理しているサービスです。 npm が管理しているものがいちばん有名です。他にも GitHub が管理する Registry が公開される予定です。

The economics of Package Management

f:id:yosuke_furukawa:20190611135113p:plain
the economics of package management

slide:

github.com

video:

www.youtube.com

「Package Managementの経済」というタイトルです。 聴講者からすると、何話すのか不明でしたが、惹きつけられるものがありました。終わったあとはこれぞ「Tech Talk」という発表でした。是非時間があれば全ての動画を見てみるとよいかと思います。

筆者はここで語られていた内容を基に entropic について紹介し、最後にこの Registry で何がやりたいのかを話します。

Successful JavaScript Registry is hard

npm のパッケージは大きくなりました。いまや100万パッケージを超えるほどです。

一方で、 npm inc は営利を目的にした団体です。これだけ多くのpackageがあると管理コストも高くなります。 npm inc の経営状況がどうこうは色々な話がありますが、実際に公開されてる情報があるわけではないので、特に何かをここで言うつもりはありません。

ただ、 Registry の管理をビジネス的に成功させるのは難しい、ということです。

npm inc の Registry は基本的に無料で使えます。無料で使う場合、すべてのライブラリは public として公開されます。 privateenterprise でグループを作るなど、特別なアクセスコントロールをするライブラリを作る時には有料版を契約するというビジネスモデルです。

npmpublicOSSのライブラリが増えれば増えるほど、 運用コストがかかります。その運用コストを private ライブラリや enterprise Registry の売上で賄います。小さいサイズの Registry は運用コストがあっても寄付金で調整するのはそこまで難しくありませんが、大きなサイズになってくると寄付金だけで運用するのはやはり難しくなってきます。これをなんとかするために、 npm inc は Venture Capital からも出資を受けています。 

Venture Capital の意向が変わったら方針も変わりますし、結果としてコミュニティにとって最適な運営が行われるとは限らないわけです。

この話は npm に限りません。 RubyGems でも Perl CPAN でも運営コストの話はあります。ただ、 npm が管理している package の数は膨大で、他の Registry よりも遥かに難しい状況になっています。

f:id:yosuke_furukawa:20190617182135p:plain
module counts

http://www.modulecounts.com/

じゃあどうするか

Registry を分散して管理できるようにしていこう、というのがこの発表の内容でした。

一つの団体が一つの Registry を集中管理している状況をやめて、分散管理するシステムを作れるようにして、みんなでちょっとずつ運用コストを分散できるようにしていこうという話です。

これの構想の基になっているのが、JSConf EUトークの中にある、 Entropic です。

github.com

Entropic: a federated package registry for anything but mostly JavaScript

Entropic は 新しい Package Registry です。 npmyarn ではなく、新しい CLI である ds も持っています。

entropic/cli at master · entropic-dev/entropic · GitHub

dsentropicregistry につながることはもちろん、 npm の Registry も read only でつながります。read onlyなので、publishはできませんが、 npm に登録されている package もダウンロードできるようになります。

ちなみにまだ experimental なので変更される可能性も大いにあります。

どうやってpackageを指定するか

dsdomain , namespace , package name すべてを指定して、ダウンロードします。

namespace@example.com/pkg-name

こういう指定方法で Package.toml に記述があると、 package がダウンロードされます。例えば、 ds 本体をdownload したい場合は chris@entropic.dev/ds と指定します。

ドメインを指定するので、別サーバで別DomainのRegistryであっても package の指定ができます。 (逆に言うと Domain を失効したらダウンロードができなくなるということですね。)

もしも npm に接続したい場合は、 legacy@entropic.dev/<pkgname>legacy ネームスペースを指定し、 entropic.dev ドメインpkgname にアクセス先の package を指定すれば npm からダウンロードできるようになります。

(おそらく entropic.devlegacy ネームスペースは npm への alias になってるんでしょう)

Docker の実行環境さえあれば、 Registry を構築することは簡単にできます。

https://github.com/entropic-dev/entropic/tree/master/services/registry#running-your-own-registry

Entropic は自分たちの手元で起動し、対象となる package ダウンロード先のhost を指定してダウンロードします。その host に自分たちが使う package のキャッシュが構築され、初回は Package.toml に記述したドメインのホストからダウンロードされますが、 2回目以降は自ホストのキャッシュを利用します。

f:id:yosuke_furukawa:20190617185414p:plain
entropic download chart

アクセス権

すべての package は public となり、誰からでも install し、誰からでも閲覧できる形になります。 private のアクセスをしたい場合は GitHub の Registry を併用する形になると思います。

また、一緒にライブラリを編集したい場合は publish 権限を他のユーザーに付与することも可能です。

https://github.com/entropic-dev/entropic#overview

Entropic のゴール

ここからまた少しエモい話ですが、 Entropic全ての JavaScript package を Entropic 上で扱おうとしてはいません。つまり、これで npm を倒すという話ではありません。

じゃあ何がゴールになるかというと、

1. 「何もしないでnpmが倒れるか、有効策が出るまで待つ」以外の選択肢を用意したい

2. 「分散管理するRegistryの運用知見」を広めたい

3. 「中央集権から分散管理」に振り子を戻したい

という話です。 npm を倒すのではなく、 npm 以外の選択肢を用意し、その運用知見を全体に広げ、分散管理していくという話は非常に分かる話で、特別な専門知識がなくても管理できるように 共通理解をみんなで深めていこうという深い話でした。

f:id:yosuke_furukawa:20190617184648p:plain
take back the commons

まとめ

1年前、 JSConf EU 2018 で、 Ryan Dahl は module や package について、深く色々な反省をしていました。

yosuke-furukawa.hatenablog.com

その中でもあったのが、「Nodeのモジュールの管理運営自体を private controlled にしてしまったこと」があげられていました。

1年後、 JSConf EU 2019 は節目の年です。コミュニティを代表するようなカンファレンスで Entropic のような発表があったことは一つの epoch making な話だったと思います。来年は一旦 JSConf EUはありませんが、数年後にどうなっているか確認する、というバトンを渡されたような思いでした。

今年 JSConf JP をやる予定ですが、この辺の話もできれば幸いです。

npm, yarn による zero install 戦略

jsconf.eu 2019 に行ってきました。 特に npm や yarn の今後の話とそもそも Registry をどうしていくか、の話があったのでお知らせします。 そもそも Registry をどうしていくかについては次のエントリで話します。

tink: A Next Generation Package Manager

npm の次のコマンドラインツールである tink が紹介されていました。

github.com

presentation: github.com video:

www.youtube.com

そもそも npm の仕組み

  1. ローカル依存ファイルを読む (package.json, package-lock.json, shrinkwrap.json)
  2. 存在しないパッケージのメタデータをfetchする
  3. 木構造を計算して、実行する(npm v3 以降だとflattenする)
  4. 実際に存在しないパッケージをダウンロードする
  5. インストールスクリプトを実行する

今のどこがダメなのか

npm は tar ball を fetch して、そこから gunzip して、最後にできあがったファイルをnode_modules以下にコピーするという非常にピーキーな処理をしています。最初のfetchは network IO に影響し、 gunzipCPU に影響し、ファイルコピーは file IO に影響します。

これを簡単にするためにキャッシュしたり、展開済みのファイルをハードリンクさせたりとチューニングをしています。 これ自身は必要な処理なのですが、実体のファイルとして作る関係上、どうしても node_modules は巨大になります。

https://cdn-ak.f.st-hatena.com/images/fotolife/y/yosuke_furukawa/20180604/20180604215229.png

tink sh

tinknode 本体の代替として起動します。

$ node foo.js

で起動するのではなく、

$ tink sh foo.js

で起動します。これで何がやりたいかというと、 node_modules を物理的なフォルダにするのではなく、仮想上のフォルダにすることを目指してます。

仮想上のフォルダになることのメリット

いくつか利点がありますが、まず実際にファイルをコピーする必要がない分、 File IO への影響は緩和されます。

また、 tink 自身はローカル内にハッシュ値を基にしたキャッシュを作るので、ハッシュ値が一致したパッケージに関してはキャッシュが使われます。  Network IO への影響も緩和されます。

また、最大のメリットとして、 tink sh で実行した際にランタイムで依存モジュールを解決するため、 npm install のコマンド実行が不要になります

つまり、 git clonegit pull してから依存ファイルがなかったとしても tink sh <cmd> で起動すれば実行時に依存モジュールを解決し、起動することができます。

virtual node_modules
virtual node_modules

この試みのことを zero install と呼びます。

prepare && unwind

本番環境で Docker 等を使って毎回ゼロからイメージを作ってる場合はイメージ作成後のキャッシュがないため、毎回起動時にパッケージのダウンロードが開始されます。

これを解決するために事前にFetchしてくるコマンド(prepare)と事前に node_modules を作るコマンド(unwind)の2つが提供されています。

$ tink prepare # 事前にfetchしてくる
$ tink unwind # node_modulesの実体を作る

load map

tinknpm v8npm コマンドに統合される予定とのことでした。

f:id:yosuke_furukawa:20190610102508p:plain

yarn v2: berry

presentation:

www.youtube.com

yarn v2tink と同様の戦略です。zero install を実現しています。 .pnp.js というファイルを内部に作成し、そこに依存モジュールを解決できるようにしています。

開発ツールとしては tinkyarn v2 も同じ戦略を取っていました。細かな機能の違いはあるのでいくつか紹介します。

yarn contraints

package.json と実行時の依存関係に矛盾があった場合に、警告した上でpackage.jsonを修正してくれるサブコマンドです。

一番多いユースケースは、実行時に依存してる package が package.json に書かれてなかったとか、 workspace と一緒に使った際にmonorepoのサブパッケージ間で異なるバージョンのライブラリに依存してた等ですね。

いわば、 package.json の linter みたいなものですね。

どうやって実現しているのか

tinkyarn v2 も実態は node のプロセスである以上、 node 側の標準モジュールでは node_modules 以下にあるファイルをロードしに行く必要があります。 これを解決するために、 tinkyarn v2 では node の標準APImodule にパッチを当てています(!)

module のファイルロードを行う箇所のメソッドを拡張し、 node_modules がなくてもファイルをロードできるようにしています。

標準のモジュールロードからは逸脱しているため、今のところ使うのは危険ではあります。将来的に読み込み方が変わる可能性もありますし、標準の node の読み込み方を変更する可能性もあるので、まだ experimental と言ったところでしょうか。

ただし、 electron も同様の手法でモジュールを読み込めるように拡張しているので、そこまで変更がドラスティックに加えにくい箇所でもありますね。

実際に、 tink の解説では、 electron も同じことやってる(ので大丈夫)というニュアンスで発表していました。

f:id:yosuke_furukawa:20190610101630p:plain

まとめ

モジュール管理は zero install 時代になっていくと思います。事前の処理を不要にすることで開発効率を上げて、 git clonegit pull から何もしなくても起動できるようになっていくと思われます。ただ一方で実際に本番で zero install にしてしまうと、起動してから実際に動作するまでのパフォーマンスは落ちる可能性もあるため、起動方法は変更になるかと思われます。

これらを解決するための本番環境用のコマンドとして、 tink preparetink unwind も公開されているので、それらを使って行くのではないかと思っています。

また一方で、 tinkyarn v2 もほとんど同じことをしているので、どちらにも有意差があるようには見えず、状況は変わらないまま今後に突入していくのではないかと思われます。

これらを踏まえて、 Registry についての話を書きます。また次回!

JavaScript が読み込まれる前でもWeb Applicationを動かす

今回は最近取り組んでいる、 JavaScript が読み込まれる前であっても「ちゃんと」 Web Application が動作するように作る話をします。

Server Side Rendering における注意点と対策

BFFを使ってServer Side Rendering をすることに数年前から取り組んでいます。

まずはSSRをやる上での注意点と対策について紹介します。

SSRをすることはSEOのためだと思われがちですが、個人的にはSEOのためにしているわけではなく、 First View を向上するため(特に First Meaningful Paint を向上するため)にやっています。

f:id:yosuke_furukawa:20190210220602p:plain
First View

SEOSSRに関しては Google が最近出したこの記事SEO Considerations 節が詳しいです。ここでは説明しません。

SSRをしない、Client Side Renderingのみの場合、この First Meaningful PaintJavaScript がダウンロードされてからになるので、遅れてしまいます。

ユーザーの環境は様々で、潤沢なwifi環境が揃っていて、最新のマシンが使えて高速という環境ばかりではありません。古いマシンを使って、インターネット環境が整備されてない状況ではJavaScriptをダウンロードする時間もJavaScriptをダウンロードしてから実行する時間も遅くなります。

色々なケースも想定し、SSR を使って First Meaningful Paint を向上することで、表示されるまでの時間の短縮を行っています。

ただし、SSRFirst Meaningful Paint を高速化する一方で、表示が速すぎると、操作するまでの時間、 Time To Interact までに乖離が発生します。

f:id:yosuke_furukawa:20190210221642p:plain
Gap between FMP and TTI

乖離が長ければ長いほど「見えてるのに操作できない時間」が長くなり、ユーザーのストレスになります

これを改善するためにはページあたりに読み込まれるJavaScriptの量を減らすことで、読み込まれる時間を改善する、 Code Splitting と呼ばれる前処理をする必要があります。また、Time To Interact の時間までインジケーターを出してユーザーに処理中であることを表示するのも有効です。

ただ、Code Splitting をしたとしても分割しすぎるとページ遷移のたびに差分のJavaScriptが必要になったり、そもそも React DOM などの巨大なライブラリに依存していると全体で読み込まれるJavaScriptgzip後で数100kbを超えることも珍しくなく、限界があります。

逆にSSRをやめて、Client Side Rendering のみにしてしまうと、「見えるまでの時間」と「操作するまでの時間」のギャップはなくなります。しかしこの場合、人間としては「見えて(認知して)から操作する」ので、ユーザーにとって最適な時間にはなりません。

JavaScript が読み込まれる前でも操作できるようにする

そこで、今取り組んでるのが、いくつかのページではJavaScriptが読み込まれる前でも普通にウェブアプリケーションとして操作できるようにしています。

HTML と Server だけでもちゃんと動くように作る

こうなると、 HTML と Server だけでもちゃんと動くように作らなければいけません。JavaScript を disabled にした状態でどこまでちゃんと動作するかを確認しながら作る必要があります。

リンクの場合

JavaScript のみで動作させる場合、リンクには click イベントをフックするハンドラを用意して、そこで pushState などのURL変更 API を使って遷移させることが多いです。

SSRレンダリングした上で、リンクなどの処理は a タグで書きつつ、 遷移先のURLを定義します。 click ハンドラはそのままにしておけば、JavaScriptが読み込まれる前に実行してもただの a タグでの遷移として動作します。

<a href="/foo" id="js-link" />

// JavaScript
document.getElementById("js-link").addEventListener("click", (e) => {
  e.preventDefault();
  history.pushState({}, "", e.target.href);
})

この辺りはSSRを作る上ではライブラリに頼ることも多いと思うので、ライブラリが提供してくれる機能でも動作します。 react-router などのライブラリも同様の機能を提供します。

form の場合

form の場合は複雑です。formの場合はSSRとは違って、 method が POST のケースも多いので、 POST を受け付けられるエンドポイントを用意する必要があります。

また、 methodaction などの form の属性もきちんと指定する必要があります。 form の method を指定しないために、全部 GET リクエストになってしまったり、ボタンをsubmitにしてなかったために、きちんと送信されないケースも散見されます。

また、 POST などの更新系の操作をする場合、厄介なのは CSRF 対策もしてあげる必要があることです。 hidden の input に対して csrf token と呼ばれるセッションに紐づくランダムな値を設定しないといけません。XHRとは違ってカスタムヘッダを渡すことも、Cross Origin のときにpreflightチェックのリクエストが飛ぶこともないので、準備が必要です。

// form に対してmethodを指定する。
<form onSubmit={handleSubmit} method="POST">
      <div>
        <input type="email" name="username" component={RenderInput} />
        <input type="password" name="password" component={RenderInput} pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"/>
      </div>
      {/* csrf 対策を入れる */}
      <input type="hidden" name="_csrf" value={csrf} />
      <div>
        <button type="submit" disabled={submitting}>
          Login
        </button>
        <button type="button" disabled={submitting} onClick={reset}>
          Clear
        </button>
      </div>
</form>

ログインのような典型的なフォームの場合はユーザーが何をするべきかが表示された段階ではわかっている事が多いため、操作するまでの時間を短縮することで、効果も大きくなります。

また、検索画面のようなフォームも入力項目が少ないため、ユーザーは表示された瞬間に迷わずにクエリーを打ち込みに行きます。このときにクエリーパラメータでURLが変わったとしても動くように作ってあげる必要があります。

このアプローチのメリットとデメリット

アプローチの内容はわかったところで、メリットとデメリットについて紹介します。

メリット

まずは性能的な面でメリットがあります。先程、操作できるようになるまでの時間と表示するまでの時間に乖離が大きいとユーザーはストレスを感じる、という説明をしましたが、JavaScriptをダウンロードするまで待つ必要はなく、「操作できるようになる時間が表示された時間とほとんど同じ」 になります。

ちょうど数ヶ月前に Netflix React の CSR をやめて、SSRでのみReactを採用した、という記事がありましたが、そこでもパフォーマンスの改善事例として紹介されていました。

アプローチとしてはほぼ一緒です。Time to Interact にフォーカスするのであれば、 JavaScript をダウンロードされてなくても動作させる方が効果的です。

次に accessiblity としても効果があります。JavaScriptに頼らずに普通に作ると verified な HTMLを書く必要に迫られます。form の method や action もそうですが、input の name や type といった属性もきちんと書かないといけません。こうすることで、矯正ギブスのような役割を果たしてもらえます。

デメリット

一言で言うと、「実装するのが大変」というところです。一度すべてのページを JavaScript をオンにして動かせるようにしたあとで JavaScript がオフでも動くようなアプリケーションにすることは実装の観点から見るとやってやれなくはないものの、大変です。

また、完璧に JavaScript が動作するアプリと同じUXを提供することは不可能です。先程のformの例でいうと、 validation などは input タグの pattern 属性にマッチしてるかどうかしか出すことができません。 JavaScript を使った validation では、もう少し細かく入力値をチェックできます。例えば「パスワードは英字、数字、記号の3種類が必ず入ってて、8文字以上」などの条件の validationpattern 属性で正規表現で書いたとしても、「パターンにマッチしたかどうか」だけしかチェックされません。 JavaScript では、「記号が抜けてる」、「8文字以下」という細かくメッセージで何が満たせてないかを表示させることで入力を補助させることができます。

リンクならともかく、ボタンの場合や、モーダルウィンドウで確認ダイアログを出したい場合など、大体においてJavaScriptが必要なケースは多く、すべてを JS が disabled にした状態で動作させるようにするのはやはり難しいと言わざるを得ません。

個人的には、ログインやトップページの検索画面など、ユーザーがある程度最初の方に触る(JavaScriptがダウンロードされる前に触りそうな)ページを JS disabled でも動作するように作り、残りは noscript タグで JavaScript がオンじゃないと動かない旨を出してあげるくらいでしょうか。

また、最後にどうしようもないところですが、解析系のタグが動作する前に動いてしまうので、ユーザーのトラッキングはできない可能性があります。この手の解析系がちゃんと動かないとNGのところも多いので、注意してください。

まとめ

JavaScript が読み込まれる前に SSR と HTML だけで動作するアプリを作ることに関しての紹介をしました。 最近の開発では、この方法を基本としており、SSRの弱点の一つである、 First Meaningful PaintTime to Interact までの差を JavaScript が読み込まれる前でも動作させることで解消しています。

ただし、すべてのページでできているわけではなく、トップページに近いような場所でのみ、部分的に動作させています。こうすることで以下のような効果を狙っています。

  1. JavaScript がダウンロードされてなくてもある程度は動作することで、パフォーマンス上のメリットを狙う
  2. HTMLでも verified な状態にすることで、 accessibility の観点や JS がオフのユーザーであってもある程度は利用できるようにする
  3. すべてのUXをJSオフの状態で提供することは不可能なので、必要なページではJSがダウンロードされるまで動作しないように作る

こうすることで、SPAのUXも提供しつつ、潤沢なインターネット環境を持っていないユーザーであってもある程度高速に表示され、動作するように作っています。

再入可能なロックの話

突然のロックの話

いきなりロックの話をしましたが、10月に(なぜか)一緒に働いてるメンバーとの中で大盛り上がりした話題です。もともとはリクルートテクノロジーズで行われている、柴田芳樹さんのプログラミングGo勉強会で話題になった話です。

yshibata.blog.so-net.ne.jp

ここにも書いてあるのですが、 Golang では sync.Mutex を使ったロックでは再入可能ではありません。 一方 Java のロックは再入可能です。

で、この設計に関しては合理的な解説が Russ Cox さんからされています。

groups.google.com

意訳すると以下のような感じですね。

再入可能なロックはbad ideaだ。

mutex を利用する主な理由は mutex が不変式を保護するためだ。
不変式というのは、 例えば 円環(linkedlistのようなもの) の 全要素 p に対して p.Prev.Next == p が成立するといった内部の不変式であったり、
自分のローカル変数 x は p.Prev と等しい、といった外部の不変式を指している。

mutexのロックを取るというのは、「私は不変式を維持する必要がある」というのと、「これらの不変式を一時的に壊す」という二点の表明である。
また、 mutexのロックを開放するというのは、「不変式にはもう依存していない」というのと、「一時的に壊した不変式は元に戻っている」という表明である。

再入可能なロックを取るというのは、いったん mutex のロックを確保した状態で再び mutex のロックを取るが、この状態では不変式を一時的に壊している可能性がある。再入可能なロックは不変式を保護しない。

再入可能なロックというのは単なる間違いであり、バグの温床になる可能性がある。

この話には納得できます。ロックの再入可能可否についてはできないようにする方が良いというのは納得できます。余談ですが、プログラミング言語Go以外の本にもEffective Javaにも同様の話がありますし、詳解Unixプログラミングでもこの手の話はあります。

じゃあなぜJavaは再入可能なのか

Javaは既存のAPI資産がスレッドセーフになっているものがあり、それらの中にはメソッドの中でロックを取っているものもあります。この状況では再入可能なロックを認めざるを得なかったため、今ではバッドプラクティスとして残っています。例を挙げると、HashTableやVectorといった既存のコレクションクラスがスレッドセーフですね。

ちなみに 詳解UNIXプログラミングの中に出てくる pthread も同じく再入可能にするオプションがあります。これも既存のAPIでスレッドセーフなものが多いから渋々追加している機能です。

というわけで、そもそも再入可能なロックを提供すること自体はGoでもJavaでもUnixプログラミングでも望まれていない訳ですね。

社内で盛り上がった内容

さて、再入可能なロックを提供しないという方針については基本的に全員同意しており、これまでの話に対しては反対意見は無いですが、よくよく考えると少し腑に落ちない点があります。それは、「不変式を守るのは、ロックの有無に限らず守らないといけないのではないか?」という点です。

この不変式を守るという話でmutexの再入可能なロックを取らなくしただけではなく、本来的には壊さないように全員が気をつけるべきであり、この問題について取り組んだのが、有名な「契約による設計 (Design By Contract)」の話です。

契約プログラミング - Wikipedia

EiffelやD言語ではこのDesign By Contract を言語仕様に取り入れているというのは有名な話ですね。

Golangでは、不変式を守るという話についてここまで説明があるにもかかわらず、Design By Contractを言語仕様には取り入れていませんし、assertすらありません。

assertを提供していない件についてはちゃんとFAQがありますが、契約による設計を取り入れてない理由も含めてGo言語がそういう取捨選択をしている理由はなんなのか、というので、話が盛り上がり、何度か柴田さんやkoichikさん、和田さんを含めて議論しました。

議論ポイントを整理すると、「ロックを再入可能にしない」という設計については納得するものの、「そこまで不変式を守ると言うなら契約による設計やassertについては入れないという選択をしたのはどうしてか」といった部分ですね。

ただどちらの主張も実は衝突するような話ではなく、「ロックを再入可能にしないのは不変式を壊したくないから、ただし、不変式を壊したくないからと行ってDesign By Contractまでのゴツい仕様は入れたくなかった」という話だろうと、古川は推測していますし、この理由である程度納得しています。

並列並行処理

色んなプログラミング言語を知っていると正解は一つではないというか多様な正解があるという事がわかります。Goの考え方はUnix哲学的なものであり、シンプルな解答を用意しつつも、バグの温床になるような言語仕様は避けようとする考え方が見られます。

一方で、この手のmutexの問題は基本的に「並列プログラミングが難しい」という問題に根ざしたものであり、これに対して何度も挑戦していないと議論ができるようなポイントに到達できないな、と改めて感じ、年末年始はこの本でも読もうかなと思いました。

www.oreilly.co.jp

Yearly Node.js 2018

Node.js 2018 まとめ

この記事は HTML5j カンファレンスで発表した、 Node.js 2018 のまとめの話をブログに起こしたものです。

speakerdeck.com

ちょっとずるいですが、この記事一つで Node.js アドベントカレンダーJavaScript アドベントカレンダーの25日目の記事です。

10月にNode.js v11 がリリース

Node.js v11 は変更点はいくつかありますが、v11.0.0ではそんなに大きな機能はありません。代わりに性能向上と安定性向上を行っています。

これにはNode.jsのコア変更ポリシーが関わっています。

Node.js Core Policyである 「Less is More」

Less is More という言葉をNode.js の文脈で最初に使ったのはこの jsconf での発表が初めてですね。

www.youtube.com

要は「豊富な機能を追加するのではなく、最小限の機能でコアは小さくシンプルにする」という話です。 もともとは建築家の言葉で、「これ以上ないことは豊かなことである」とも訳されます。

さて、この発表の中でも大きな機能が少ない代わりに、フォーカスすることとして、安定性・性能・セキュリティを向上させることについて触れています。

今回はNode.js が2018年の中で、どういう形でこれらの非機能要件とも言える情報について改善を重ねてきたかを解説します。

安定性

安定性と一口に言ってもいろいろあるのでここでは、Node.jsが安定性を得るためにやってることを紹介します。

LTS

Node.js は長期間サポート(LTS)があります。

https://github.com/nodejs/Release/raw/master/schedule.png

このサポートはLTS対象のバージョンであれば2年間のパッチリリースが確約され、セキュリティのアップデートは3年間受けられるというものです。

今だとv6.xが4月までセキュリティアップデート期間、v8.xが4月までバグ修正のアップデート期間です。v10.xは予定では2020年の10月まで受けられます。

V8 の ABI 互換性サポート

Node.js の内部のJavaScriptエンジンであるV8はNode.jsに対しての非互換の修正があるときには事前の通知がされるようになっています。 また、V8自身はアップデートがある度に、Node.jsに対して最新のmasterを追加し、テストが落ちないかを定期的にモニタリングしています。

github.com

Jenkins によるCI実行

Node.jsでは、PR毎に各種CPU, OSのビルドをJenkins上でCIを回すことで壊れるかどうかを確認しています。

jenkins-node-ci
jenkins-ci

ただ、Node.jsのテストも膨大な数があるので、testが実行されると中には PASS したり、 FAIL したりする flaky なテストがあります。これらについてはレポートされる仕組みがあり、その中で詳細な調査を行いながら修正されていきます

flaky test
flaky-test

つい最近 flaky なテストが全部PASSして、グリーンになった!ということでコアメンバーが喜んでました。

(ただこのツイートにもあったとおり、明日にはすぐにイエローになってしまいましたが。)

アプリケーションを安定化させる試み

大きな機能追加はないですが、アプリケーション内でCPU負荷が高いときのinspectorや非同期処理実行時にtraceする async_hook といった機能が追加、改善されています。

Inspector | Node.js v11.5.0 Documentation

Async Hooks | Node.js v11.5.0 Documentation

これらの機能はnode.jsの内部状態をもとに解析するツールです。CPUのプロファイラはV8の内部プロファイラの機能を利用して状況を把握するためのツールです。こういった機能が増えることで、安定性を改善しています。

const inspector = require('inspector');
const fs = require('fs');
const session = new inspector.Session();
session.connect();

session.post('Profiler.enable', () => {
  session.post('Profiler.start', () => {
    // invoke business logic under measurement here...

    // some time later...
    session.post('Profiler.stop', (err, { profile }) => {
      // write profile to disk, upload, etc.
      if (!err) {
        fs.writeFileSync('./profile.cpuprofile', JSON.stringify(profile));
      }
    });
  });
});

性能

Node.js においては性能も重要な指標の一つです。これも維持するために色々やっています。

ベンチマークを常に測る

benchmarking グループというワーキンググループがNode.jsのパフォーマンスを常に計測しています。

benchmark
benchmark

これを見ながら大きなregressionが起きていないかとか、バージョン間で差を見ることもできます。基本的にV8がパフォーマンスを向上させているので、省メモリになっていたり、高速になっています。

また、マイクロベンチマークだけではなく、現実のアプリケーションでもどれくらい下がっているかを計測するためにExpressを使った航空機予約システムである、acmeair という模擬システムでも評価しています。

GitHub - acmeair/acmeair-nodejs: A Node.js implementation of the Acme Air Sample Application. With datastore support of MongoDB, Cloudant, Cassandra. With runtime support of Bluemix/CloudFoundry, Docker... With Micro-Services.

Worker

Node.js の昨今の利用例として、ネットワークサーバだけではなく、babel, webpackといったフロントエンドのツールとして使われることが多いです。この様な時には大量のファイルを変換したり、文字列連結をしたりするので、IOの時間よりもCPUの時間が支配的になります。結果としてマルチスレッド・マルチプロセスでの処理の方が効率的にCPUが利用できます。

Node.js: The Road to Workers

Turbo Boost Next Node.js - Speaker Deck

実際に筆者もbabelを使ってmulti-threadとmulti-process、シンプルに一つのプロセスを使ったもので比較してみました。筆者の計測結果を以下に載せます。

worker result
worker result

これを見ると、ファイル数が100以上であればマルチプロセスよりもマルチスレッドのほうが高速になるという結果が出ました。プロセスを起動するよりもスレッドを起動するほうがコスト的に若干安いので、こういう結果になりますが、まだSharedArrayBufferは利用していないので、メモリ共有をしだすとどうなるかはまだ考察していません。

ちなみに最近入った llhttp というパーサがやばい

最近入った Fedor Indutny 製の HTTP Parser ですね。

github.com

これ、えげつないっす。

HTTP Parser はこれまで C で書かれた http_parser が使われてました。 しかしながら、 Cのhttp_parserは中身を見るとメンテナンスしやすいとは言えず、またアクティブなメンテナもいなかったので徐々にブラックボックス化していました。

Fedor の作った llhttp は「JavaScriptで書いた処理をLLVMバイトコードC言語に変換することでHTTPのパーサを作ってしまう」というものです。正確にはTypeScriptで書かれており、TypeScriptで書いた処理をC言語LLVMバイトコードに変換しています。

これ、普通だと逆で、C言語 / LLVM で書かれたものを JavaScript でも呼べるように asm.js や wasm に変換する」というアプローチを取りそうですが、 Fedor は「 JavaScript で書いた処理を C言語 / LLVM に変換」しています。

中身を読むと分かりますが、実際にはCやLLVMのジェネレータがあります。Node.jsで試すなら、build optionで --experimental-http-parser を付けてビルドするか、 実行時に --http-parser=llhttp とやると最新の Node.js では実行できます。

llhttp
llhttp

セキュリティ

Node.js でも昨今問題になっているセキュリティについてもコアでの取り組みを紹介します。

セキュリティワーキンググループ

Node.js 内部では TSC と呼ばれるコアの内部で話し合いが行われています。毎回セキュリティのトピックは話されており、特に OpenSSLや V8 といった内部依存ライブラリの脆弱性があると事前に関係者だけに告知され、パッチの適用後、全体に通知されます。

nodejs.org

あんまり知られていませんが、 Bug Bounty プログラムも行われています。これにより、セキュリティの脆弱性をついたバグには報奨金が支払われるようになっています。

hackerone.com

セキュリティリリースがあると以下のように告知されます。

nodejs.org

セキュリティの取組み(npm, yarn)

コアのセキュリティではありませんが、3rd party製のライブラリでもセキュリティ障害が見られることがあります。 記憶にあたらしい所で行くと、 event-stream が別メンテナーによってセキュリティの問題を仕込まれた事がありました。

github.com

このような問題は急成長している npm のモジュールだと発生しがちです。対策としては今の所事後策で自分のリポジトリ内に問題があるかを調査することしかできません。そのようなコマンドを npm も yarn も用意しているので、積極的に使っていきましょう。

https://docs.npmjs.com/cli/audit

https://yarnpkg.com/lang/en/docs/cli/audit/

この手の npm|yarn audit コマンドを実行すると、自分の package-lock.json や yarn.lock 内にあるリポジトリ脆弱性の報告がないかを検証してくれます。

Web Standards

「Less is More」といっても例外があります。 Web 標準のAPI に関しては機能追加しようとする動きがあります。

コアコミッターの一人である James Snell さんの話にあった言葉を紹介します。

why node.js needs web standards
why node.js needs web standards

「Node.js は主にウェブアプリケーション開発者プラットフォームとしても今までも、今も存在している。一方で Node.js のコアは small core という哲学を表明している。この small core の中には Web Standards によるものも含まれている。」

これらの流れから、HTTP2 や ES Modules 、 Promise の改善といった機能追加は Web 標準の API と合わせるために行われています。

Promisify や fs.promises は Promise 改善の流れです。

util.promisify が追加された - from scratch

File System | Node.js v11.5.0 Documentation

また、 HTTP/2 から HTTP/3 までの流れも検討はされています。

ngtcp2をベースに HTTP/3 の JS 実装をしようという検討は書かれています。

今後の流れ: Unified JavaScript Platform

今後は JavaScript の共通プラットフォームとして統合していこうとする流れがあります。

Unified JavaScript Platform
Unified JavaScript Platform

現在は、 Web Standard API に関しては W3CWHATWG といったグループが作っています。Node.js の Standard API は我々 Node.js core memberが作っています。また、それらの中間にある JavaScript そのものの API や文法は ECMA/TC39 といったグループが作っています。

これらのグループには特にコンセンサスが取られているわけではなく、それぞれがそれぞれで緩く繋がっていましたが、今後はこの繋がりを強化して、もう少しお互いのコンセンサスを取りながら統合していきたいという話が NodeFest 2018 の Node Discussion でされていました。

W3C / WHATWG で作られたAPI と Node.js の API は歩み寄りをしていき、なるべく寄せていきます。また、その中央で ECMA/TC39 が仕様を固めるという風に三者がまとまりながら話を進めていくようにしていきたいという話がされていました。

Web API も Node.js APIECMAScript も求めているのは "ユースケース" です。さらにWeb API も Node APIECMAScriptも全部丸っと知っているのは仕様策定者よりも開発者になります。開発者、つまり僕らがライブラリやアプリケーションを作ってユースケースを作っていき、仕様策定者側にフィードバックしていく必要があります

つまり、リードしていくのは、仕様作成者だけではなく、我々です。

2019年は Node 学園祭は jsconf.jp として生まれ変わる予定ですが、そこでも仕様フィードバックの場を設けて今後の未来を一緒に作れるようにしていきたいとおもいます。

蛇足

この手のNode.js と JavaScriptアドベントカレンダーの活動も今後は JavaScript アドベントカレンダー一つにして、記事数が足りなくなったら「その2, その3」と増やせるようにしていきたいですね (というわけで Unified Advent Calendar Entry にしてみました)。

Chrome Dev Summitに参加しました!

Chrome Dev Summit に初参加しました!色々トピックとして気になったものを紹介してます。後直接 Addy Osmani とか Paul Irish とかに聞く機会があったので、色々ついでに聞いてきました。

Chrome も 10 周年なんですよねー。感慨深い。

1日目は「現在のChromeでできること、やってること」という感じで、ケーススタディやツールチェインの話が多めでした。 2日目は「未来のChromeでできること、今後やるべきこと」という感じでした。

1日目の熱かったもの

ProjectVisBug

github.com

特定のサイトに対して画像を変更したり、一部のコンテンツの内容を改変したりすることがGUIを通してできるツールですね。

f:id:yosuke_furukawa:20181116195400g:plain

意外と簡単にインストールして、ドラッグアンドドロップやフォントサイズの変更がGUIで変えられるので、オーサリングツールみたいな感じですね。普通にインストールしておくといいと思います。見た目をさっと変えてイメージ伝えるのに良いですね。

Squoosh

squoosh.app

画像のエンコード形式を変更したときの見た目をbefore after形式で見ながら自分の画像の最適なものに変更してくれるツールですね。ツールとしても有用ですが、ツールとしてというよりも中身がすごいですね。 WASMでwebpやmozjpegなどの各種エンコード形式をブラウザで変換してますし、それだけじゃなく、web workerを使っていたり、service workerを使っていたりとモダンな機能をふんだんに使ってますね。

f:id:yosuke_furukawa:20181116200037g:plain

ツールとしてはもちろん、今のOff the main thread や PWA や WebAssembly の全部入りのプロジェクトとして興味深かったです。

Performance Budget Case Studies

昨今のトピックであるPerformanceの話ですね。今回はPinterest だったり、Spotify だったりとパフォーマンスのケーススタディが多く、最近の仕事に近いので参考になりました。

特に Performance Budget という考え方が最近は主流になりつつあるので、各社の対応がどうなってるかが知ることができたのは非常に有用でした。

f:id:yosuke_furukawa:20181115225922p:plain

日経電子版以降、こういう事例が増えましたね。

ただ一方で、ちゃんと気をつけないといけないのは、「パフォーマンスを上げることが即ビジネスのKPIに繋がる」という話はビジネスのKPI次第なので危険ですね。いろんなファクターがビジネスKPIに関連する中でパフォーマンスを上げることはベースラインの品質を上げることはできてもビジネスKPIとは直接起因するファクターになるかは微妙です。

Chrome Dev Summitの話の中で面白かったのは、「Long Term のビジョンを持ちつつも、 Short Term での計画を作っていく」という点です。いきなりビジネスKPIを求めるのではなく、長期的なKPIを上げるというビジョンと短期的な成果を上げていくのが重要というのは水を浴びせられるような気持ちになりました。

f:id:yosuke_furukawa:20181115230703p:plain

この話が無茶苦茶刺さった。。。やっぱ一度パフォーマンス改善をやってみると分かりますが、そんな簡単にビジネスKPIに繋がらないんですよね。

『速くなったことでユーザ体験はよくなったとしてもユーザ体験を上げることが即売上につながるわけではない』、というか。ただし、じゃあやらなくていいかというと違っていて、それは目に見える成果につなげていくための階段の一歩目でしかなく、もっと着実にステップを踏んでいくことで最終的によくなるという話にしないといけないわけです。

2日目の熱かったもの

2日目からは Addy Osmani と直接話したり、色んな人と話に行っていた関係で、途中で退出したものもあったのですが、それにしても面白い発表ばかりでした。

Off the main thread

main threadを使わずにworker threadを如何にうまく使うか、というところに焦点を当てた話ですね。ここのトピックでもいくつか話はありましたが、まずはweb workerのpain pointの話からスタートします。

f:id:yosuke_furukawa:20181116180429p:plain

web worker はメッセージパッシングモデルで実施されていますが、メッセージのやり取りの際にserialize と deserialize が交互に発生します。このやり取りを thread hop と呼んでいて、thread hopがたくさん発生するとNGという話が書いてありました。

f:id:yosuke_furukawa:20181116180347p:plain

これに対して、 Tasklet と呼ばれるライブラリが提案されていました。これは Task 間での協調を簡単にするためのライブラリですね。まだ experimental とのことです。

f:id:yosuke_furukawa:20181116180317p:plain

更に言うと、 Worker にした所で手放しに良くなるわけじゃありません。

f:id:yosuke_furukawa:20181116180743p:plain

Worker にすると rendering はスムースになりますが、入力のレイテンシは遅れます。メインスレッドを阻害しないというだけで直接高速になるとかそういうものではない、ということですね。

worker dom や amp script と呼ばれる amp の仕組みでは、メインスレッドを阻害しないようにしており、このトレードオフをうまく使って実現されているとのことでした。

f:id:yosuke_furukawa:20181116181019p:plain

この他にも Actor Model などの考え方が紹介されており、UIをどう作るかが変わってきそうだなと感じました。

WebPackaging

f:id:yosuke_furukawa:20181116200329p:plain

WebPackaging の Signed Exchange というコンテンツに対して署名して、コンテンツオーナーであることが確認できればURLバーをコンテンツホルダーのURLに変更できるという仕組みが紹介されていました。

f:id:yosuke_furukawa:20181116173440p:plain

特にAMPで有用な技術ですね。AMPでは、Googleの保持するAMPのキャッシュサーバからコンテンツを配信することができますが、現状ではAMPのキャッシュサーバのURLは配信サーバのURLになってしまうため、これが問題視されています。

Signed Exchange はこのURLバーをコンテンツホルダーのURLに変更することが可能な技術です。コンテンツホルダーが署名し、その署名がコンテンツホルダーのものかどうかを検証することで実現されます。まだ仕様策定中の技術です。

Portals

次のページの navigation を seamless にするという話で Portals が紹介されていました。

f:id:yosuke_furukawa:20181116174216p:plain

SPA が提供するものの一つとして、部分レンダリング(header, footer等は変えず、main contents 部分のみをレンダリングする事)による、 seamless なページ遷移がありますが、これを SPA じゃなくても可能にしてしまう技術ですね。ページを先読みし、先読みしたことをイベントとして受け取ることができるのと、それを iframe のような形で表示することができます。別ページであっても部分レンダリングしてるかのように見せることができるのがすごいですね。

実際の活用例として、集英社の漫画を閲覧する機能を Portals で使った例が示されてました。

f:id:yosuke_furukawa:20181116173850p:plain

この他にも タスクの優先順序をスケジューリングすることができる、 Schedule API や Picture In Picture といった機能が紹介されており、 SPAじゃないとできなかったことが徐々にブラウザでやってくれるようになってきているなぁ と思いました。

WebPackaging と Portals を組み合わせることで「これまでサイト間やページ間で感じていたギャップを縮め、low friction から zero friction へ」という話がされていました。

f:id:yosuke_furukawa:20181116200516p:plain

インタビュー

折角の機会なので、色々な人に話を聞いてきました。Google の Paul Irish, Addy Osmani, Alex Russelと少しだけ話してきました。

Interview with Paul Irish

lighthouse などのパフォーマンスツールの開発者である Paul Irish と話してきました。

Q1. Performance Budget という話は多いが、既に React, React DOM, React Router, axios などを使っていると 60KB くらいは有り、さらにその上で moment.js などのライブラリや Polyfills が乗ってくると 300KB とかを超えてしまうが、 Paul Irish はどうしているか?

A1. 基本的にあまりライブラリは使わず、使うとしても重たくないのを選んで使うか、ブラウザの素の機能を使う。DateTime系の機能もdate-fnsにするか、もしくは intl の DateTime APIを使ってしまう。また、最近だとPolyfillsも限定的な環境 (IEとか)でしか使わないので、普段はbabelでtranspileもしない。

Q2. babelを使わないといけないのはいくつかあって、一つは JSX 、 もう一つは ES Modules なんだけど、どうしているか?

A2. 作ってるのがツール系だからあんまり使わないけど、しょうがないときは使う。

Q3. 社内で性能測定ハッカソン(ISUCONみたいなの) をやっていて、その時に headless chrome を high load performance tool として負荷かけつつ、チェックするのにも使おうと思ってるんだけど、プロセスが毎回起動するので負荷をかけるツールとしては使いにくい、もっと軽量なものを予定していないのか?

A3. あったほうが良いかも。ただやるなら Web SpeedTest みたいな感じでちゃんとしたクライアントから実行する仕組みでそれを複数台から実施する方が実際の環境には近いはず、作るのは大変かもしれないけど、作ってくれたら事例になる。

f:id:yosuke_furukawa:20181116174413p:plain

Interview with Addy Osmani

Q1. 会社の中でlighthouse をCIに使っているが、 lighthouse の点数を参考にしているだけで、他にメトリクスとして取るべき指標はあるか?

A1. JS Size, CSS size, image みたいなリソースのコストは取っておいた事例があるかも。

Q2. 事例で言うとJSはどれくらいのサイズにしている or するべきなのか?

A2. JS で言うと、 Pinterest は 200KB 以下という指標を持っている。昔は170KB以下が望ましいとされてたが、今は250KB程度までは問題ないと言われてる。 web.dev を有効活用して欲しい。

Q3. Guess.js みたいな考え方のものを使って先読みをしようと思ってるけどどう思うか?

A3. Guess.js は alpha 版だけど、あれは考え方みたいなものだし、そのまま使って欲しいと思っているわけじゃない。何か統計的な情報に基づいて先読みする仕組みは重要なのでやってみて事例にすると良さそう。

Q4. WebのフロントでPerfomanceを改善してみているが、最終的にビジネスKPIに繋がるような話にまでならないことが多い、この辺をどう考えているか。

A4. まさにそれはGoogle全体で課題だと思ってる。一方で「パフォーマンスを良くしたらビジネスKPIが上がる」というふうに短絡的に捉えるのは危険。パフォーマンスが悪ければ品質の問題になるが、良ければ必ずしも売上やユーザ数が上がるといったKPIに繋がるわけではない。いくつかのサブとなる指標を置いてやってみるのが良いと思う。

Q5. Pinterest がセッション時間が伸びて回遊時間が伸びたのを取ってたみたいな形?

A5. そのとおり。セッション時間が伸びたというのでユーザ 1人あたりが滞在する時間が増える、というのを指標にしている。

f:id:yosuke_furukawa:20181116174625p:plain

interview with Alex Russel

(帰りかけてる時にちょろっと話しただけで、全然ちゃんとは話せず・・・、また写真も取れず。)

Q1. ブログのエントリで「JS は CO2 」とか「DXはおとり商法」みたいな記事を読んだよ。あの話の中で理想とするJSのフレームワークはあるのか、または何も使わず plain なJSでやるのがいいのかはどう思ってる?

A1. JSのフレームワークは必要だと思ってる。ただブログに書いたとおりすごくtiny なもののが良いと思ってる。 preactとか svelte は理想の一つ。

まとめ

1日目は今のWebでできることを紹介しつつ、ケーススタディで非常に面白かったです。2日目は未来のWebでできることを紹介していました。どちらもワクワクする内容で、久しぶりにすごく刺激になるカンファレンスでした。

また、2日目の最後の方に Google の中で Web Performanceをやっている人たちと刺激的な話ができたのも面白かったです。来年もやるとのことですし、Google IO もあるのでまた行きたいですね!

Node.js における設計ミス By Ryan Dahl

Ryan Dahl は Node.js の original author ですが、彼の作ったプロダクト deno に関するトークが jsconf.eu 2018 でありました。 Node.js にずっと関わってきた僕が見て非常に興奮するような話だったので、しばらくぶりにブログに書き起こすことにしました。

背景

Ryan Dahl は2009年に Node.js の話を初めて公の場に公開しました。その時の「公の場」というのが「jsconf.eu 2009」です。

www.youtube.com

Video: Node.js by Ryan Dahl - JSConf.eu - 2009

この発表から Node.js が広まり、今やサーバのみならず、IoTデバイス、デスクトップアプリなど、様々なところで動作しています。

で、今回はその発表から9年の歳月が経過し、Node.jsに対しての設計不備について Ryan Dahl 自ら発表したという状況です。

2018.jsconf.eu

発表資料: http://tinyclouds.org/jsconf2018.pdf

動画:

www.youtube.com

今回の記事はこの話を超訳したものを紹介し、慣れてない方のために都度解説を挟みます。最後に古川の感想を書いて締めようかと思います。

当初のゴール

私 (Ryan Dahl) はNode.jsの初期開発と開発マネージメントを行っていました。当初の目標は「イベントドリブンなHTTP Serverを作れるようにすること」でした。 ある時点から Server side JS というゴールにスイッチしていきました。Server side JSにはイベントループモデルを取り入れることに成功しました。

WindowsでのIOCPと Linuxでのepoll、 OSXではkqueueを融和させ、libuvを作った事、それをcoreのJSレイヤでサポートしたり、npmを作ってユーザのコードを管理したりという一定の成功は得られました。

私は2012年にリーダーを引退し、Node.jsの開発を引き継ぐことにしました。『2012年の時点でもう既にゴールは達成された』と思っていました。しかし、今現在2018年にNode.jsを半年間使ってみたところそれは勘違いでした

Node に残っていたmission criticalなタスク

2012年頃にIsaacにリーダーを引き継いだ後、実際はまだまだいくつかのcriticalなタスクが残っていました。 それらのタスクは消化され、今のNode.jsは当時よりも良いプロジェクトになっています。

  • npmをnodeのcoreの中に入れること by Isaac
  • N-API というbinding API
  • libuv の構築 by Ben and Bert
  • governance と community の管理 by Mikeal Rogers
  • crypto周りのコードベースの大幅な改善 by Fedor Indutny

この他にもいろいろな物がありましたが、現メンバーの力によってかなり大幅な改善がされています。

しかし、私は「現時点の Node.js を半年間使ってみたが、自分の目的とは異なったものになってしまった」という感想を持っています。

mission critical なタスクはいくつかは解消されていましたが、いくつかのタスクはそもそもの設計の根幹に関わっており、解消しきれていませんでした。 このあとの話はそれらをリストアップし、どうやって deno が生まれたかについての話になります。

動的型付け

動的型付け言語は科学的計算を一度だけ行うのにはベストな言語です。 JavaScript は動的型付けの言語の中でもかなり良い方の言語だと今でも思っています。

ただ Node.js においては複数回トライエラーを繰り返しながら設計されることを想定しています。設計過程で起きたNode.jsのエラーの内容はわかりにくく、エラー時の解消方法もはっきりしていません。

Node.jsのこういう側面を見るたびに黒板を爪で引っ掻いた音を聞いたときのような嫌な印象を受けました。

せめて今ならもっとよくできるのではないか、という思いを持ちました。

古川注釈: 静的型付け

おそらく静的型付け言語のがエラーの内容がわかりやすく、解消方法もわかりやすいと言いたいのではないか、特にTypeScriptのように変換する事が現代は割と一般的なので。

Promise

Promiseは2009年に一度Node coreに記述されていました。しかしながら、2010年にはそれらをすべて消すということを決定しました

今でも愚かな決断だったと思っています。

async/await の抽象化を行うためには Promise は最初から入れておくべきでした。

Node.js のコアではPromiseが無いために今日まで非同期に関しての体験を悪くしてしまっています。

古川注釈: Promise

2009, 2010年のPromise騒動は覚えていて、Promiseを入れるという事が行われていたにも関わらず、当時はcallbackのほうがprimitiveでわかりやすく性能面でも利点が大きいという事になり、Promiseにするのはcoreの実装では不要という判断がされていました。

当時はここまでPromiseが今後のキーになるとRyanは思っていなかったのでしょう。

Security

V8それ自身はsecureなsandboxモデルを表現しています。この点についてもっと深く考察しておけば、もっと良いセキュリティの考察を得られ、それによって他の言語よりもより良いセキュリティを提供できたと思ってます。

例えば: ただのlinterなのに、networkアクセスやファイルへのフルアクセス権は不要ですよね。

古川注釈: Security

V8それ自身はただJavaScriptの実行エンジンであってファイルシステムへのアクセスやネットワークアクセスする機能は提供していません。標準出力を行う console.log ですら V8の対象の外です。それに対してNode.jsは fshttp といったファイル・ネットワークリソースへのアクセスを提供します。ただ現在のNode.jsの幅広いユースケースを鑑みると、Linterやbuildツールとして実行しているときにネットワークリソースへのアクセス権は不要であったり、ファイルの書き込み権限は不要であったりします。

これらをsandboxの中で適宜パーミッションを得ながら実行できればもっとより良いセキュリティモデルを提供できた、と言いたいのでしょう。

Build System (GYP)

GYPは大きな失敗でした。ビルドシステムは思ったよりも難しくて重要な根幹のシステムでした。

そもそもv8がgypを採用していて、Node.jsもその仕組に乗っかりましたが、その後 gyp から gn に build システムは移り、gypユーザーは取り残されてしまいました。

gyp のインタフェースはそこまで悪いものではないですが、JavaScriptのプロジェクト内で「JSONっぽいけどJSONじゃないPythonのsyntax」を使わされ、ユーザーにとってはひどい体験だったでしょう。

このまま gyp を未来永劫使い続けるのは Node.js のコアにとっては大きな失敗です。 V8 の C++ bindings を書くなら、今は FFI (Foreign Function Interface) を推奨します。

かつて、 FFI を推奨してくれた人たちが居ましたが、当時の私(Ryan)はそれを無視しました。

(ちなみに、 libuv が autotools をサポートしたことに関しては今でも残念に思ってます。)

古川注釈: gyp

FFI と gyp に関しては FFI のがポータブルである一方で当時はgypのが高速という事が言われていました。性能を優先したデザインを取ってgypをサポートしていました。

しかし、 gyp は python2 ベースですし、そもそも Chrome や v8 開発のために作られたビルドシステムです。Chrome や v8 のビルドシステムも現在は gn や bazel というビルドシステムに置き換わっていて、 Node.js がgypを使い続けるのはアーキテクチャ上負債になっています。

コアが特定プロジェクトのビルドシステムに依存するよりは FFI のような統一的な呼び出し方法のがまだ良かった、と言いたかったのではないかと思います。

(ちなみに Ryan は常に何かのビルドシステムに乗っかっては「失敗だったから変えるわ」って言うタイプで昔はWAFというビルドシステムにのって、それからgypに変えています。)

package.json

Node.js は package.json の中にある "main" フィールドを読んでそれを Node.js に require() でモジュールとして読み込ませることにしました。

最終的には npm は node.js の中のreleaseに含めることに成功し、それがデファクトスタンダードになっています。しかしながらそれは中央集権リポジトリを生み出してしまいました。

require("somemodule") と記述するのは明確な特定のモジュールを示しているわけではありません。 ローカルに定義されたモジュールの可能性もあります。npmのデータベースにあるモジュールなのかローカルに定義されたモジュールなのかは実は呼び出しているだけではわかりません。

f:id:yosuke_furukawa:20180604213609p:plain

また、 package.json はファイルを含んだディレクトリをモジュールの概念として扱っています。これは厳密的に言えば必要な抽象化ではありませんでした。 Web 上には少なくともその抽象化されたモジュールは存在しません。

また、不要な情報を多く含んでいます、ライセンス、リポジトリURL、description、こういう情報はプレーンなモジュールにおいては noise です。

import した時にファイルとURLsが使われていれば、パスにバージョンを定義できます。依存関係のリストも不要です。

古川注釈: package.json

いくつか示唆に富んだ話ですね。解説が難しいです。ここの話からモジュールの話やpackageの話が多く存在します。 現地で聞いていたときの印象としては Ryan Dahl は module は package.json 以下にあるディレクトリを指すのではなく、コアが提供するのは単一ファイルで十分なのではないかと思っているんだと思いました。

import の時に import foo from "https://example.com/foo/v2/index.js" とかで url で指定させたり、 import foo from "./foo/v2/index.js" などのファイルで指定できるようにするだけでよく、ディレクトリを指定させるのはアプリケーションごとにやれば良いと思っているのかと。

node_modules

f:id:yosuke_furukawa:20180604215229p:plain

moduleの解決アルゴリズムが相当複雑になってしまいました。 vendorのモジュールをデフォルトにした事は良いことではありますが、実際には $NODE_PATH は使われていません。

ブラウザのsemanticsからも外れてしまいました。

これは私のミスです、本当に申し訳ありません。でももうやり直すことは不可能です。

古川注釈: node_modules

module の解決アルゴリズムは node_modules 内の依存関係をたどって依存解決をします。また、 moduleは指定元からの相対パスでファイルを指定しますが、 $NODE_PATH 環境変数にパスがセットされているとそこからも読み込まれます。

この他にもいくつか hacky な方法がありますが、どの方法も微妙です。この状況を招いたのは node でもっとシンプルな方法を提供できなかったせいだと Ryan は語っていました。一方で今のNode.jsの仕組みをブラウザに逆輸入されているため、この意見については反対意見もあるようです。

".js" の拡張子なしで module を読み込ませられるようにしたこと

いわゆる require("somemodule").js の拡張子がなくても Node.js はモジュールとして読み込み可能です。 でも browser では src の指定に対して .js を省くようなことはしません。

module loader はユーザーの意図を表現するファイルシステムへのクエリーであるべきで、明示的な指定のが良いです。

index.js

index.js は require("./") で読み込めますが、これは require("./index.js") の省略です。 最初はこれが良いアイデアだと思ってました。ブラウザも //index.html を省略できます。

しかしながら、これも module loader を多少複雑にしました。

Node.js module 設計ミスサマリ

私の失敗の多くは module に関する部分です。どうやってユーザのコードを管理するかという部分の失敗が多くありました。

なぜこの状況になったかというと、イベントI/O に集中して注力してしまい、結果としてモジュールの設計を後回しになってしまった事にあります。

この重要性に早く気づいていればもっとこの状況をよくできたと思っています。

Deno

私はこれまでの設計ミスに基づいて新しいプロダクトを開発しました。それが deno です。

github.com

最初に言っておくと、全然まだまだプロトタイプレベルです。 動かないのが普通の状況なので、 lldb でデバッグして直したりしない限り動かないし、ましてやこれでなにか作るのは強くオススメしません。 (※ ちなみに古川はまだ osx で起動どころかビルドに成功していません。)

deno は v8 で動くセキュアなTypeScript 実行環境です。

Deno のゴール: Secure Model

deno は sandbox モデルになっています。デフォルトではネットワークアクセスもなければファイルの書き込み権限もありません。 (--allow-net --allow-writeをつけない限りはネットワークアクセスと書き込み権限が付きません。)

ユーザーが信頼していないツールをちょっと動かすみたいなケースではこれはセキュアなモデルです。(例えばlinterをちょっとだけ動かすとか)

また、 deno では「ダイレクトに任意のnative codeを実行すること」は許可していません。全ては protobuf の呼び出しによって間接的に実行されます。

以下の図を見てください。

f:id:yosuke_furukawa:20180604215242p:plain

古川解説: Secure Model

deno は OS のカーネルのごとく、 特権モードとユーザ空間を明示的に分けてるデザインなんですね。非常に面白いです。 Goの中で v8-worker と呼ばれる worker を起動して、 worker と main process の間で protocol buffer を経由して通信して特権のコードを実行するか決めてるんですね。

(面白いけど、ここまでのセキュアなものが本当に必要なのかは不明ですね。一方で最近 npm の脆弱性も増えているので必要な面もわかります。)

Deno Goal: モジュールシステムのシンプル化

Nodeの既存モジュールとの親和性は求めてません。

importは相対パス絶対パス、もしくはURLだけでしか指定できません。

import { test } from "https://unpkg.com/deno_testing@0.0.5/testing.ts"
import { log } from "./util.ts"

import には拡張子は必要で、基本的に一度読んだらキャッシュしますが、キャッシュを強制的に開放するときは --reload をつけて実行します。

vendoring に関してはデフォルトのキャッシュディレクトリの指定をしなければ実現できます。

古川解説: モジュールシステムのシンプル化

ファイルをモジュールの基本的な単位にするし、node.jsのnpmのことは完全に忘れて新しくするという潔さ。 また、 import には URL かファイルの相対・絶対パスしか用意しないので、 vendoring というかバージョン管理や固定は不要ですね。ファイルならそのままgitで管理できているし、URLの場合は絶対パスなので取得先が壊れたりしない限りは(理想的には)固定されます。

まぁただ本格的に使うなら vendoringとかpackage management 用の何かの仕組みを3rd partyが作ったりするんでしょうけど。 vgo とか bundler のように。

TypeScript コンパイラ

私はTypeScriptが大好きです。

TypeScript はとても美しく、プロトタイプレベルから巨大なシステムになっても構造化を保つことができます。

denoではTypeScriptのコンパイラをモジュールの参照解決とビルド成果物のインクリメンタルキャッシュに利用します。 TypeScriptのモジュールは変更されてない限りは再コンパイルしません。 通常のJavaScriptも使えるようになります(TypeScriptはJavaScriptのスーパーセットなのでそこまで難しくはありませんが)。

スタートアップを高速化するためにv8のsnapshotも利用する予定です(まだプロトタイプには入ってません)。

古川注釈: TypeScript コンパイラ

Ryan Dahl が TypeScript が好きなのは一つ前のRyan Dahl プロダクトである propel が TypeScript なところからも感じていました。ここはそこまで驚きはないです。 何個かのアプリケーションを作るうち、Ryan Dahl も型が必要だと思ったのでしょう。

v8 snapshot は ここで解説しましたが、所謂heapのsnapshotを事前に取っておいて起動を高速化するためのテクニックですね。

single executable file と最小限のリンク

deno それ自身が最小限の構成をすることを目指しています。 ldd で見てみると7つ程度しかファイルがありません。

 > ldd deno
linux-vdso.so.1 => (0x00007ffc6797a000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f104fa47000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f104f6c5000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f104f3bc000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f104f1a6000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f104eddc000)
/lib64/ld-linux-x86-64.so.2 (0x00007f104fc64000)

2018年であることを活用したモデル

Nodeのモジュールをコンパイルするのに Parcel をバンドルツールに使っています。Nodeで行っている事よりも最大限にシンプルです。

github.com

native code の中に 良いインフラストラクチャを作っています。httpサーバについても心配する必要はありません。既に動作は確認できています。 (Nodeの時はWeb Serverは今は手で自分で作る必要がありましたが、今は違います。)

また現時点ではJS以外のパートは Go を使っています。しかし、これはGoですべてやるという事でもありません。今は色々調査しています。 Rust や C++ も良いでしょう。

その他諸々

  • キャッチされない例外がPromiseで起きたら即座にシャットダウンします。
  • top-level await をサポートします。
  • 機能的に重なる場合はブラウザとの互換を取ります。

古川の感想

deno はまだまだプロトタイプレベルですが、今のNode.jsにもフィードバックできるところは多そうだなと思いました。そういう意味では使ってフィードバックをNode.jsにしていこうとは思いました。しかし今時点では積極的に使おうにも lldb 等のデバッガなしでは使えないので何かを作れるようになるのは先だなと思いました。

またNode.jsにおける module とは package のことであり、ディレクトリを指してます。一方で ES Modules や deno における module というのはファイルの事であり、最低限の単位しか持ちません。この時点で考え方において大きな違いがあると思いました。

Node.jsがディレクトリを単位とし、 package.json という定義ファイルがあったからこそ議論を進めた一方で、言語やコアのベーシックな機能として持つ最小限の module というのが何なのか、何であれば良かったのかについて考えさせられました。

いずれにせよ 9年越しに Ryan Dahl の話を聞けて大分面白かったです。他の jsconf と Node Collaborators Summit の話も面白かったのでいつか書きます