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 の話も面白かったのでいつか書きます

NaN === NaN が false な理由とutil.isDeepStrictEqual

NaN === NaN は false

NaN、つまりは Not a Number 同士の同値比較が false になるのは、よく JavaScript とかで罠だと言われていますが、罠でもなんでもないです。 false が返るという仕様です。仕様の経緯を追うとすぐに『 IEEE754 という浮動小数点の標準規格で決められているから』、という理由がヒットします。

では IEEE754 ではなんで NaN == NaN を false にしようという話になったのか、というのを調べてみました。 今回はそういう歴史の話です。

IEEE754

現在のプログラミング言語の処理系の多くが採用している浮動小数点の標準規格です。

この標準規格は以下のことを定義している。

- 基本形式: 二進および十進の浮動小数点数データの集合。有限な数(符号付ゼロと非正規化数を含む)、無限、特殊な「数ではない」値(NaN)から成る。二進形式3種類、十進形式2種類で、計5種類の基本形式が存在する。
- 交換形式: 浮動小数点数を効率的かつコンパクトな形で交換するのに使われる符号化形式(ビット列)
- 丸め規則: 算術や変換の際に数を丸める方式(端数処理)。5種類
- 演算: 基本形式に対する算術演算や他の演算
- 例外処理: 例外的状態の通知(ゼロ除算、オーバーフロー、その他)。5種類

またこの規格では、高度な例外処理、追加的な演算(三角関数など)、式評価、再現可能性などを強く推奨している。

wikipediaより。

wiki項目の中で NaN だけは特別な記述がされています。

NaNとの大小比較では、自分自身と比較した場合でも「大小不明な結果」を返す。

NaN - Wikipedia

ただ厳密に言うと、 NaN には signaling NaN と quiet NaN という二種類あり、 JavaScript の NaN はquiet NaN という 「NaNを比較した時に例外を上げない代わりに必ず false になる」というものですね。

なんで false なのか、という経緯

標準規格で決まっているのはわかったものの、なんで NaN === NaN を false にしたのか、というそもそもの経緯についても調べてみました。 NaNとは、『数学的に数字ではない、とされている値を計算機上で扱うときの便宜的な値』です。比較不可能な値同士を比較したという事でその時点で本来的には例外です。

ただし、 JavaScript では quiet NaN が採用されているため、例外はスローされず、 false になります。

同じように Infinity という『数学的に無限を表す便宜的な値』もあります。Infinity === Infinity は true になっていますが、これにもちゃんとした理由があります。

IEEE754 で表現しているのは丸めも含めた"近似値"になります。

IEEE754-2008 という改訂版では、 +∞や-∞への丸めも記述されています。

方向丸め
- +∞への丸め 正の無限大に近い側へ丸める。切り上げ (rounding up, ceiling) とも呼ばれる。
- −∞への丸め 負の無限大に近い側へ丸める。切り下げ (rounding down, floor) とも呼ばれる。

なので、 Infinity も他の数字と同じく丸められた近似された値です。『無限大』というNumber.MAX_VALUEですらない、上限を表すための近似値のため、 Infinity === Infinitytrue になる、それに対して、 NaN は近似された値ですらありません

近似されてもいない数字ではないもの同士の比較に対して、『 NaN === NaN が見かけ上同じものだからという理由で true になるのはおかしく、 false であるべき』、というのが IEEE 754 での主張です。ここまでが NaN === NaN が false な理由です。

+0-0 の比較演算

さて、 === の比較だと厳密等価という値比較になり、 NaN同士 を比較した場合は false になるというのは前述の通りですが、=== にも多少微妙な数字が有ります、それが +0 と -0 です。

IEEE754において、比較演算では +0-0 は等しいとされており、 +0 === -0 は true になります。しかし、実際の2進数上の表現は異なります(符号ビット部分)し、 1.0/0.01.0/-0.0 で得られる値も異なります(前者は Inifinity, 後者は -Inifinity )。ほとんどのケースでは +0 === -0 が trueでも困りませんが、比較以外の演算の時のみ+0と-0は分かれて扱われており、それらを無限小(無限大の逆)として扱うのであれば +0 === -0 が trueになるのはおかしいという話もあります。

IEEE 754における負のゼロ - Wikipedia

ES2015で追加された Object.is というのはこれを正しく処理するためのものです。 +0 と -0は Object.is() では false になります。

※ ただし、 NaN 同士の比較でも true になります。 Object.is(NaN, NaN) => true

IEEE754の制約を受けずに機能的に同一の値(SameValue)であれば true になる関数という事ですね。機能的に同一、という言葉だけでは分かりにくいのですが、要は NaN を含んだ配列で NaN が存在するかをチェックしようとするケースや、 Object.definePropertyで -0を指定するケースで利用するものです。

[1, NaN, 3].indexOf(NaN) // 必ず -1
[1, NaN, 3].findIndex((e) => Object.is(e, NaN)) //  1

Object.is は一般的な開発者が使うというよりも多少メタな領域で JavaScript を拡張したいライブラリーとかが使うもの、という認識ですが、憶えておいて損はないです。特に +0-0を分けておきたいときには有用です。

util.isDeepStrictEqual とは

object 同士の内容を比較する便利関数です。 v9 から追加されています。Node.js Advent Calendarでも紹介しました。

qiita.com

で、この util.isDeepStrictEqual が多少変な動きをするので、議論を重ねていたら、 NaN === NaN が false な理由とか JavaScript の同値には3種類あるとかそういう沼にハマって調べていた、というのがこの話を書こうと思った経緯です・・・(長かった)。

このツイートの元になったのは、 util.isDeepStrictEqualWeakMap|WeakSet の時に内容がどうであれ必ず true を返すという動きをするためです。

x y === Object.is util.isDeepStrictEqual
1 1 true true true
"foo" "foo" true true true
NaN NaN false true true
Infinity Infinity true true true
+0 -0 true false false
{ foo: 1} {foo: 1} false false true
{ foo: 0} {foo: -0} false false false
new Set([1, 2, 3]) new Set([1, 2, 3]) false false true
new Set([1, 2, 3]) new Set([4, 5, 6]) false false false
new WeakSet([{foo: 1}, {foo: 2}]) new WeakSet([{foo: 1}, {foo: 2}]) false false true
new WeakSet([{foo: 1}, {foo: 2}]) new WeakSet([{foo: 3}, {foo: 4}]) false false true

util.isDeepStrictEqualに関しては、Strict Equal という名前がついているので、厳密等価性 (=== )を表現するのかと思いきや、そうではなく、機能的に同一であるという方の Object.is と基本的には同じ動きをします(注意点1)。型が objectMapSetArray の時は中身を deep に比較するという動きを見せますが、 WeakMapWeakSetの時は内容が取れないので、 valueOf の値が空オブジェクトになるため、中身がどうであれ必ず true が返ってきます(注意点2)。

この動きについていくつか issue を上げてみて、様子を伺っていますが、 TSC としては議論中です。将来的に動きが変わるかもしれないので、v9時点では util.isDeepStrictEqualをヘビーに使うのは推奨しません。

個人的には StrictEqual という名前から想起しやすい動きをしてくれたほうが良いので、NaN 同士や +0, -0の動きも === と同じ動きのが良いです。このままにするのであれば、 util.isDeepSameValue という名前にしてほしいと思ってます。

WeakMap/WeakSet に関してはGCに依存した動きをしますし、そもそもコレクション同士を比較できるものじゃないので true よりは false もしくは例外をスローするか undefined みたいな特殊な値のが正しそうだな、と思っています。

参考資料

2017年振り返り

2017年振り返り

毎年激動の年ですが、今年も色々あったので振り返りをしていきます。

会社

リクルートテクノロジーズに転職して1年、マネージャーになりました。

codeiq.jp

正確にはマネージャーとシニアソフトウェアエンジニアの兼務なのですが、マネージャー側面の話はあまりしたこと無いのでこの CodeIQ のイベントは貴重でしたね。

チームビルディングの記事も書きました。 recruit-tech.co.jp

会社で仕事してるの?って聞かれること多いのですが、めっちゃやってます!!!!コードも書いてるし、マネジメントも一応ちゃんとしています。まだまだおもしろい仕事があるので興味あれば言って下さい!!!!

イベント・登壇

日本でのイベントや登壇はあまりカウントしてなくて、海外イベントの登壇は今回2回行いました。

How to create a local JavaScript Community in Node Interactive North America 2017 www.youtube.com speakerdeck.com

Turbo Boost Next Node.js in JSDC Taiwan 2017 www.youtube.com speakerdeck.com

英語での発表も苦手意識は薄くなってきましたが、やっぱりまだまだ緊張しますね。日本での登壇と違ってアドリブだったり失敗した時のリカバリができないので緊張しながらやってるのが分かります。

日本での登壇で一番評判がよかったのは以下の発表でしょうか。

You need to know SSR speakerdeck.com

PublicKey にも記事にしていただきました。 www.publickey1.jp

Node Community

Node 学園祭

今年も無事やれてよかったなと思いました。自分の思いが先行してていくつも失敗はあったのですが、発表・ワークショップ・ディスカッション・アフターパーティと結果としては大成功だったかなと。

yosuke-furukawa.hatenablog.com

Node Japan User Group 法人化

今年一番大変だったものの1つなんですが、Node Japan User Group を法人化し、 Japan Node.js Association にしました。

Japan Node.js Association

ぜんぜん慣れないながらも定款を書いたり、法務局を何度も訪問したりと色々やっていました。法人って時間がとにかくかかるのですが、去年からの宿題でずっとやらないといけないと思っていたのができたのがホント嬉しかったです。法人化すると法人データベースに名前が載るのですが、載った時はガッツポーズしましたね。

法人化もできて、今後も大きくなる土台作りが済んだのでNode.jsをますます発展できるようにこれからもがんばります。

インタビュー系記事

今年はインタビューで取り上げて貰う機会が多くてありがたかったです(なぜか緑の服着てることが多い)。

codezine.jp

freelance.levtech.jp

logmi.jp

codeiq.jp

app.codegrid.net

app.codegrid.net

執筆系

完全に色々滞りました。すいません、、、すいません、、、

React の記事を Web DB Press に納めましたが、こちらも厳密に言えば2016年の年末に頑張っていただけなので今年は執筆業としてはそこまで書いてこなかったかも。。。

gihyo.jp

一番大きな懸念であった法人化が終わったので来年は執筆も頑張っていきます。

OSS

Node.js の HTTP/2 に色々コントリビュートしてましたね。

https://github.com/nodejs/http2/commits?author=yosuke-furukawa

HTTP/2 周り全然最初動いてなかったので動くようにしてテスト追加してっていうのをやってたらv8でexperimentalながらリリースされました。(∩´∀`)∩ワーイ

でもこれもまだまだですねぇ、HTTP/2は次のv10までにexperimentalフラグが取れるように色々活動が進んでいますが、一番やりたいパフォーマンス面での貢献がまだまだ。。。

あと、Node.js のコアに関しては簡単なものは会社の若手ができるように社内で Node.js Core にコントリビュートさせる活動を開いています。この辺が活動の成果ですね。

https://github.com/nodejs/node/pull/17734 https://github.com/nodejs/node/pull/17699

活動していたら会社の若手が発表してくれました。嬉しかったです。

speakerdeck.com

Node.js コア以外だと agreed と呼ばれるライブラリをメンテナンスしたり、 redux 周りのライブラリをメンテナンスしたりしてました。

github.com

github.com

github.com

mizchiくんtwadaさんkoichikさん といったJavaScriptOSSが得意な同僚に囲まれて刺激的な環境で開発できるのでOSS開発を仕事として実施する事ができてすごく良い体験をしています。

ブログ系

この辺の記事がバズりましたね。

yosuke-furukawa.hatenablog.com

yosuke-furukawa.hatenablog.com

yosuke-furukawa.hatenablog.com

書いている内容もどんどん専門的な話になっていて、難しくなってきているのですが、これまで以上に分かりやすく書けるように頑張ります。

まとめ

今年もブログを書いて、Node.jsに貢献して、発表して、という一年でしたが、会社での地位も大きくなってどんどん色々な事に貢献していけるようになりました。来年は会社でエンジニアファーストな技術グループを作っていくことに一層磨きをかけつつも、今年出来なかった執筆系にも力を入れつつ、これまで以上に技術的な所を頑張っていきたいと思います。

来年もよろしくお願いいたします。