この DOM がすごい2018: worker-dom

おもしろライブラリを見つけて興奮しているので紹介します。

UIスレッド(メインスレッド)からユーザー操作をブロックしてしまうような重い処理を逃がす off-the-main-thread を実践しようとなると、実際に問題になるのは、ほとんどの処理は何らかの形で DOM を参照し、それに連なるものが処理時間の殆どを占めている、ということです。

off-the-main-thread の時代 - mizchi's blog

DOM に触れない WebWorker でビジネスロジックを処理するのは、ある種の健全性(Universal/Isomorphic)を手に入れるための「縛りプレイ」として有用ですが、現状は実用上のメリットが殆どありません。

例えば react / redux の reducer で、ビジネスロジックを worker 側に移して処理できるぐらいアイソモーフィックに(DOMに触れずに/nodeでテストできるように)記述することは可能ですが、 redux の処理がボトルネックになることはほぼなく、基本的にその後のReactの内部的な更新処理がユーザーの処理をロックするのがほとんどです。React/Vueのようなライブラリの処理をWorker側に逃したいと思っても、DOMと密接に絡んでいる以上、現状はその手段がありません。

worker-dom

そんな中、AMP Project から面白いものが出てきました。

ampproject/worker-dom: The same DOM API and Frameworks you know, but in a Web Worker.

WebWorkerでDOMを実行できるようにする、という実装です。

動くのはこんなコード

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="/dist/index.mjs" type="module"></script>
</head>

<body>
  <div src="hello-world.js" id="root">
    <div class="root"><button>prompt</button></div>
  </div>
  <script type="module">
    import { upgradeElement } from '/dist/index.mjs';
    upgradeElement(document.getElementById('root'), '/dist/worker.mjs');
  </script>
</body>
</html>

ここまでは Main Thread での Worker の起動部分

const root = document.createElement("div");
const btn = document.createElement("button");
const text = document.createTextNode("Insert Hello World!");

root.className = "root";
btn.appendChild(text);
root.appendChild(btn);

btn.addEventListener("click", () => {
  const h1 = document.createElement("h1");
  h1.textContent = "Hello World!";
  document.body.appendChild(h1);
});

document.body.appendChild(root);

(APIがなんかダサいですが、アルファ版ということで…)

このJSが worker 側で動いて id="root" の DOM が操作される、ってことですね。

これがなぜ AMP からでてきたかというと、AMPは色々とビジネス的な側面もあってややこしいのですが、基本的にはベストプラクティスの強制によってWebの体験を良くしたい、というのが彼らの発想です。例えばレンダリング最適化の為の CSS インライン化であったり、レンダリングブロッキングするような JS の禁止だったり…

で、AMP Project は別に好き好んでJSをブロックしている訳ではなく、彼らが嫌っているのは JS によるメインスレッドの専有です。なので Worker で DOM 操作すればいいのでは?という発想の元、 worker で DOM を動かす、というライブラリを試験的に試しているんでしょう。AMPから出てきたといっても、特にAMPでしか使えないというものではないです。(そもそもサブセットなので)

好きなJavaScriptをAMPで実行できるようになるかも。Web Workerで実現か? | 海外SEO情報ブログ

どうやって Worker 上での DOM を実現しているか

というわけでコードを読んできました。

基本的には、 Worker側に実装した MutationObserver をメインスレッドに postMessage して適用する、という感じです。

この記事の前段階として、コードリーディングのメモがあるので置いておきます。 worker-dom-code-reading.md

Worker 側の変更を Main Thread に伝える

JavaScript で MutationObserver 含む DOM API を全部実装してるのが正気ではない感じですが、Googleならではの腕力という感じ…

UI Event を Worker に送る

  • Main Thread で発生した event をキャッチして、シリアライズして Worker に送る
  • Worker 側で イベントスナーを発火させる

この結果、Worker にまるで DOM があるかのように振る舞わせる事ができます。コード読むとWebWorkerの名前空間に self.document などを露出させていて、JSDOM でテストを書くときなどの雰囲気に近いですね。

つまり、今までこうだったのが

こうなるということです。

この UI Thread のJavaScript では UI Event を Worker に渡すだけで、Scripting 処理自体は初回にworkerを起動して、あとは postMessage するだけなので、基本的に Worker 側で全てのロジックを捌きます。例えばReact/Vue の仮想DOMの生成/比較のようなボトルネックになってた部分も解決する可能性があります。

demo見る限りは preact の実装があります。コードを読んでいても React を動かすには〜みたいなコメントが散見されるので、意識してるみたいですね https://github.com/ampproject/worker-dom/tree/master/demo/preact-todomvc

この手法の問題

  • event.preventDefault() のような同期的にイベントバブリングを中断する処理が実現できない
  • window.getComputedStyle のように、レンダラから値を取り出すような処理が出来ない(Worker側にレンダラがない為)
  • postMessage で JSONシリアライズ/デシリアライズのコストが余分に掛かる
  • MutationObserver がJSなので、ネイティブ実装に比べて遅いのではないか(要検証)

preventDefalut の部分は本質的な問題で、web-components でそもそもバブリングしないボタンを作るとか、Facebook の Yoga エンジンみたいなレンダラをWorker側に実装してしまうとか、そういう荒業が思いつかなくはないのと、縛りを受け入れて書くのは不可能ではないのですが、なんとかなってほしい…

展望

見た感じ筋が良さそうに見えます。裏側に偽のDOMを置いて、その差分だけ実体に通知するという発想は仮想DOMと似ていますね。

もしこれが実用可能になったら、既存のJSをそのまま移すという用途ではなく、ライブラリの下回りで使ってユーザーは気付いたら WebWorker 名前空間にいた、という体験になるんじゃないでしょうか。

追って調査して、実用できそうになったらまた報告します。

WebAnimation の為のタイムラインエディタを試作してみた

ペルソナ5みたいな UI 作りたいみたいな話あって、メニュー画面の動きだけでも作れないか、と主要な動線だけ雑にReactでスケッチしてみたが、早々に限界が訪れた。

理由はいろいろあるが、(無限に気合でやれば終わるが) (そもそもどこにゴールに設定するかおいといて…) 細かいタメを作るのにフレーム単位の制御が必要。現代のHTML5には Flash Studio の代替がないという問題があり、本格的じゃなくていいからそれっぽいやつを作れないかと思って、試作してみた。

動画

ここで試せる https://5bbe44153813f06f1ff69d0c--timeline-editor.netlify.com

  • クリックでアンカーを撃てる
  • アンカーをカーソルキーで値とタイミングを調整できる
  • width のみ
  • easing も linear のみ

GitHub のコード (きたない)

github.com

中身

Web Animation に食わせるJSONを生成している。CSS Animation と比較して、JS から細かく制御できるので今回の要件に合っている。ただモダンブラウザでもほとんど実装されてないので、ポリフィルを使っている。ポリフィルでだいたい動く。

qiita.com

吐き出したJSONをこういう風に食わせると動く。

    const timelines = [{...}];
    for (const tl of timelines) {
      const { keyframes, ...others } = tl
      el.animate(tl.keyframes, others)
    }

UIは手打ちSVG

今後

スコープを限界まで小さく、本当に単機能なものなら作れる…かもしれない。が、本気で作ろうとした場合のだるさも既に見えている。とくにドラッグアンドドロップでアンカー移動とか考えると辛い。レンダリングの最適化のことも考えると頭が痛い。

金もらってるわけじゃないし、気分が乗ったら作るかもしれない。

ところで Patreon を募集しております。

www.patreon.com

Redux 再考

今まで自分で作ったものが十数個、仕事で5社ぐらいの redux を見てきたので、その結果思うところを書く。

前提として、自分はエコシステムに乗るという意味で今では redux 肯定派だが、redux それ自身が過剰に抱えている複雑さはもっと分解されるべきだ、という立場。

Redux がうまく設計されているとどうなるか

  • 一貫した一つの設計論に従うので、考えることがなくなる
  • 難しさが廃されるのではなく、難しい部分が一箇所に集中する。React Component の末端では、何も考えることがなくなる。状態管理という難しい部分を作る人と、末端のコンポーネントのデザインに注力する人を分けられる。
  • 大規模になっても設計が破綻しにくい、というエンタープライズ向きな特性を持つ。が、その技術基盤は(静的)関数型由来の考えが多く、基礎設計や基盤理解にはハイスキルが要求され、需要と適用対象のミスマッチを感じることはある。結果として、そもそもハイスキルを前提として、大規模であることが自明な SPA 向きということになる

現実の Redux はどうなっているか

  • 「実際に起こること」は大したことがないが、「物事が起こる経路」が難しい。教える側とすると、関数型由来の概念ばかりで、説明するコストが高い。そもそも redux そのものが elm 由来で、 React 自体も関数型的 API デザインの傾向があり(不変性を根拠にした差分アルゴリズム)、 Redux はさらに過激に関数型由来の概念を必要とする(reducer という 実質 State Monad 的な何か, 副作用の分離、様々な関数合成、関数を返す関数、高階コンポーネント、etc...)
  • Middleware に何を使うかで、 redux ユーザー同士の非同期のノウハウは共有できず、分断されている(thunk, saga, steps, epic, no middleware)
  • 初学者にとって、 初期ボイラープレートが大仰で印象が悪い。巷にあふれるチュートリアルも、悪印象を助長するものが多い。ボトムアップに学ぶ人には、最終的な形態を最初に見せる必要はないと思うのだが…
  • 現実問題、「それ本当に redux が必要?」という問いに、答えを窮する採用事例が多い。React Native や Electron のような SPA ではわかりやすく必要と言えるが、普通のウェブサイトに大仰な Flux が登場しうるかは、局所的な複雑度がどれほどかによる
  • 静的関数型由来の概念が多いくせに、TypeScript / Flow での型付けが困難な API、という実用上の問題を抱えている(redux.combinerReducers)
  • サーバーサイドの人が使うと、DB 側のテーブル定義に従って reducer を分割しすぎる傾向がある。このせいで複数の reducer の中身を跨ぐ処理が書きづらい問題と相まって、結果として使いづらく感じる、という自滅傾向をよく見る。個人的には reducer は画面に対し一つで、あとで段階的に分割するほうがいいと思う
  • 他の Flux フレームワークはほぼ全滅したので、選択肢は実質 redux or 我流の 2 つしかないのが現実。エコシステムに乗る、採用と教育コストを下げる、Web の人間を ReactNative に耐えうる設計をできるようにトレーニングする、という点で、それ自体の必要性が薄いが redux を採用するというのは、実際ある。
  • SPAという技術そのものを目的にすると失敗する。ただ、あらゆるプロダクトチームは、自分たちのプロダクトに求めるUXを過大に設定する。だからこそ会社が興ってプロダクトがあるわけだが、外からは滑稽に見える。これは技術とビジネスの本質的な問題。

今後も主流であり得るのか

  • redux の作者でありオピニオンリーダーだった Dan Abramov は、現在 redux との距離を置いているように見える。React コアチームに近い、よりシンプルなものを、という立場になっている
  • 実務上優秀なライブラリと、普及させたい側にとって都合がいいライブラリは異なる。難しい概念を振り回す redux が React 側に疎まれてるんじゃないか、と感じることがある。例えば react-training 社の react-router が、簡単側に倒してAPI削りすぎて不興を買ってる
  • React 公式チーム的には HoC より render prop 推しで(New Context API の実装 example がそう)、 Dan Abramov もその気配がある。他に、GraphQL ライブラリの apollo の React バインディングなどの実装でも redux の関与を減らすような、それ自身で非同期や状態管理を行う render prop ベースの API が実現されており、 Redux 不要論の根拠の一つになっている
  • React v16 の New Context API そのものは Redux を倒すものではなく、むしろ Redux を効率的に実装するための手段なのだが、「状態をビューにマッピングする」という概念自体を再考する段階で、別のものが出てくる可能性もある
  • React v17 で React 自体が非同期処理の仕組みを持とうとしている(Suspense)が、非同期を簡単に書くためのツールとして使いたい側と、CPU パフォーマンスが良くないときのパフォーマンス向上に注力するための非同期というコアチームで、既にミスマッチがある。ただ非同期処理のやり方が固まってくると、 redux 側の設計指針も変わってくるだろう。
  • 現状 redux を超える React の設計論はないが、React v17 でContext, Suspense 込みで再考されるときに、新しい設計論がまたいくつか出てくるはず

React から見た Vue 周りのエコシステムについて

  • Vue は React 界隈で枯れた概念をコアチームに近い人たちが実装する傾向があり、良き二番煎じという振る舞いがうまい(Redux => Vuex, Next => Nuxt)。Facebook は基本的にコミュニティのライブラリを公認しない結果、エコシステムの主流が定まらずに混乱する傾向がある。特にルーター周辺。社内ではチームごとに好き勝手にやってる様子。
  • React / Redux の関数型的な不変性が厳しい世界からみると、Vue / Vuex の状態管理はかなり怠惰で弛緩しきってるように見える。学習には容易だがスケール時の不安がある。(みたいな話を @potato4d とした記憶がある)
  • ユーザー基盤から見ると、英語圏 vs 非英語圏という構図が見え隠れしている。本体周辺は英語ドキュメントがあるが、Vue の周辺ライブラリは中国語のドキュメント / Issue / バグ報告しかないことも多いのが、個人的に採用を躊躇う要素
  • React は難しいが一貫している(Simple)のに対し、 Vue はいろんなことが簡単にできるが一貫性がない(Easy)

TypeScript入門以前ガイド

某社で自分が React/Redux + TypeScript などの講習をやってみた結果、TypeScript 入門用資料が必要だと思って書いたやつです。

このドキュメントのターゲット

  • TypeScript で書かれたプロジェクトに参加する人
  • TypeScript を導入するために、その事前知識が必要な人

自分が React/Redux などの講習でいろいろやってみた結果、 ES2015 と TypeScript を同時に教えると、初学者は何がどの概念に由来するかの区別が出来ずに混乱します。なので、ES5 -> ES2015, ES2015 -> TypeScript にわけて解説することにします。

先に言っておくと、 TypeScript 固有の機能というのはほぼ存在せず(ほぼ enum のみ)、ES2015 の個々の文法に対して、型アノテーションの文法がちょっとずつ拡張されているだけです。

このドキュメントの読み方

前提: プログラミングの基本がわかること(四則演算、変数、関数などの概念)

  • JavaScript を詳しく知らない => ES2015 for Beginners 編へ
  • ES5 + jQuery の人 => ES2015 for ES5 Programmers 編へ
  • ES2015 はわかるが、 import/export がわからない => ES Modules 編へ
  • ES2015 + ES Modules がわかる => TypeScript 編へ
  • 実運用を知りたい人 => エコシステム編へ

最初に ES2015 の解説をして、次に TypeScript がそれをどう拡張するか説明します。

ES2015 for Beginners

あなたはプログラマーとして JavaScript を初めて学ぶ人間です。プログラミングの基本的な概念は知っています。もしかしたら JavaScript を少しいじったことがあるかもしれませんが、大きなアプリケーションを書いたことはありません。

この言語は複雑怪奇な(残念な)歴史を持っており、いろいろな都合で謎の慣習があったりなかったりしますが、新しく学ぶ人は ES2015 と名乗る仕様だけを追えばオッケーです。むしろ、2015 年より古い情報は、目を通さない方が良いぐらいです。

今の JavaScript は、ES2015, ES2016, ES2017... と毎年言語の仕様が更新されます。ES2015 は特別なバージョンで、このリリースサイクルになる基点で、かつ大規模な機能追加が行われたバージョンだからです。ES とは EcmaScript の略で、ブラウザによる拡張などを除いた、純粋な言語機能だけを指します。

IE 以外のブラウザ(Chrome, Safari, Edge, Firefox)は、いずれも 2 ヶ月から 6 ヶ月の間隔で更新され、どのブラウザも ES2015 と呼ばれる水準なら、基本的にカバーしています。

しかし、現実には 2018 年においてはまだ IE をサポートしたい、するべきという意見が多数派でしょう。 そのため、JavaScript の開発者コミュニティにおいては、ES2015 で書いたコードを ES5 に変換して配布するのが主流です。IE のサポートが切れる 2021~2024 年まで、この状況は続くことでしょう。

この記事ですべての文法を解説しませんが、日本語でのES2015の入門は、 JavaScript の入門書 #jsprimer を推奨しています。

知っておくとよい雰囲気

  • 基本的な文法自体は,比較的シンプルな C/Java の系譜
  • 関数が number や object のようにファーストクラスオブジェクトであり、つまり変数に代入可能。関数参照なので、オーバーロード不可
  • 歴史的経緯によって Java から多数の予約語を借用しているが、Java との互換性はない
  • 歴史的経緯と後方互換性によって、非推奨な機能が多々残っている。具体的には with(...) {...}, var による変数宣言, == による比較など。
  • 関数型を目指した時期(ブレンダン・アイクによる初期実装)、Java を目指した時期(廃止された ES4 と ActionScript との分岐)、Python に影響を受けた時期(Mozilla による JavaScript 1.x)、CoffeeScript 経由で Ruby に影響を受けた時期(Arrow Functon, class), TypeScript 経由で C# に影響を受けている時期(最近)がある
  • 使う人によって、書かれるコードの雰囲気がかなり異なる

ES2015 for ES5 Programmers

あなたは一昔前の JavaScript で生きてきた人間です。 jQueryAPI は一通り知っています。サーバーで生成した HTML にクラスをつけて $(...)セレクタを捕まえて、その中身を書き換えてきました。$.map(...) による制御や、 var self = this のようにクロージャの this を保存して再利用してきたかもしれません。

ES2015 入門するにあたっては、ES5 までに覚えた「お約束」は、全部忘れてください。

  • var による変数宣言は推奨されなくなりました。再代入しない値は const、再代入を許可する変数は let を使ってください。ほとんど const でいいはずです。 const - JavaScript | MDN
  • window.App = {...} のようなグローバル変数によるモジュール渡しは行いません。トップレベルスコープでの代入も暗黙のグローバル変数への推奨されません(onload = function() {...})。ファイルスコープと、 import/export による参照渡しによって行います(ES Modules 編を参照)
  • jQuery の DOM 操作は、昔はブラウザ間の差異を吸収する役割がありましたが、今では標準 API で十分で、そちらが推奨されています。たとえば $(".js-items") ではなく、document.querySelectorAll(".js-items") を使ってください。 You Might Not Need jQuery
  • var self = this は、 arrow function によってほぼ不要になりました。これは、function() {...}() => {...} と書ける機能なのですが、この関数スコープ内の this は親スコープの this を引き継ぎます。 アロー関数 - JavaScript | MDN
  • ES2015 の ES Modules 環境においては、トップレベルの thisundefined です。 window ではありません。
  • 非同期表現は jQuery.Deferred ではなく Promise を使ってください。Promise による API が利用可能ならば async/await を使ってください。jQuery3 以降の jQuery.Deferred は Promise と互換になりましたが、レガシー環境では jQuery 2 以上の採用は稀です。 Promise を使う - JavaScript | MDN
  • 複雑な prototype 継承は(表向きには)行わなくました。 class 宣言 を使ってください。内部的には prototype 継承イディオムと同じことが行われています。 クラス - JavaScript | MDN
  • ネットワーク通信は jQuery.ajax ではなく、window.fetch が推奨されます。ただし、fetch 自体が冗長な使い方を必要になるケースが多く、post を多用する場合は axios の方がよく使われます。いずれも Promise ベースの API です。 Fetch 概説 - Web API インターフェイス | MDN axios/axios: Promise based HTTP client for the browser and node.js

ES Modules

ES Modules(略称 ESM) は JavaScript に他の言語のようなモジュールシステムを導入する機能です。

import - JavaScript | MDN

import/export によって、外部のファイルパスを指定することで、export されたオブジェクトを呼ぶことが出来ます。また、ファイルごとにファイルスコープを持ちます。(僕が知ってる言語の中では python の module system が一番似てます)

// src/a.js
import b, { c } from './b.js'
b();
console.log(c);

// src/b.js
const v = 1; // 外から見えない

// default は特殊化されています
export default () => {...};
export const c = v;

依存が静的に決定されるので、グローバル変数同士で依存がある場合と違って、初期化順の問題が発生しません。(循環参照だと未解決のundefinedになることはある) IE 以外のモダンブラウザでは、<script type="module" src="main.js"></script> という風に呼ぶと、 ESM でコードを書くことが出来ます。

ただし、機能として実装されているだけで、実際にはあるファイルをエントリーにした時、「非同期なファイル取得 => パース => 依存決定 => 非同期なファイル取得 => パース => ...」という処理を繰り返してしまうので、お世辞にも最適化されてるとは言えません。依存を静的に解析して配信サーバーを最適化するための仕様がまだ安定していないためです。(将来的にHTTP/2 Push をベースに読み込みを最適化する仕組みが考えられています)

IE で動かないこと、読み込みの最適化が行われないこと + npm のモジュールを依存に含みたいことが多い、といった理由で、Webpack というツールで、一つのファイルをエントリーにして、一つのファイルに固めてしまうことが現状ベストプラクティスとされています。

webpack

おまけ: CommonJS について

2011~ に書かれたコードと、NodeJS のコードは module.exports = ...require('...') による ESM ではないモジュールシステムを見ることがあります。これらはファイルシステム相対パスでの参照解決という点では ESM と似ていて、実際に ESM の仕様策定に大いに影響があったのですが、これを browserify というツールでクライアントでも node.js のエミュレーションをする、というツールが流行った結果、これをモジュールシステムとして採用するフレームワークが増えました。

node.js では未だに ESM の扱いが experimental なので、「モジュールシステム以外は ES2015」というライブラリも比較的よく見かけます。

今盛んに使われている webpack も初期バージョンでは ESM を一旦 commonjs に変換して解決していましたが、今のバージョンでは import を直接解釈して結合しています。

Webpack によって、ESM と commonjs は相互読み出しが可能になっているのですが、 commonjs では default に相当する概念がないので、扱いがやや特殊になっています。commonjs から export default {} を参照したい場合 require('./foo').defaultという形になります。ただし、Webpack による特殊化であって、標準化された振る舞いではありません。

非同期表現: Promise と async/await

(やや難しいので必要になるまで読まなくて良い)

PromiseJavaScript で非同期処理を表現するための機能です。

「1 秒待つ関数」を表現するのは次のようになります。

const wait = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(), 1000);
  });

wait().then(() => console.log("wait done"));

reject(...) を呼ぶと失敗処理になります

const willFail = () =>
  new Promise((resolve, reject) => {
    reject(new Error("reason..."));
  });

willFail()
  .then(() => console.log("never"))
  .catch(() => console.log("come here"));

ES2017 では、これを構文的に表現する async/await という文法が追加されました。

const main = async () => {
  await wait();
  console.log("after 1000");
  try {
    await willFail();
  } catch (e) {
    console.log("come here");
  }
};

main();

内部的には Promise(と正確には ES2015 で追加された ジェネレーター関数) の糖衣構文です。

async で宣言された関数の中では、 await で Promise の解決を待ちます。 await の中での非同期例外は 例外機構 try {...} catch(error) {...} で表現されます。 また、この関数の返り値は必ず Promise のインスタンスになります。関数が終了すると resolve される Promise オブジェクトです。

トップレベルスコープでの await は今現在、仕様で許可されていませんが、将来的には可能になる予定です。Chrome の DevTools では例外的に許可されています。

基本的には、非同期関数は Promise で表現しつつ、使う側は async/await で解決する、という形になるでしょう。

TypeScript

TypeScript の型は、基本的には JavaC# と似ています。Javaの影響を受けた C# の作者 Anders Hejlsberg が設計した JavaScript 拡張です。

入門資料

TypeScript in 5 minutes · TypeScript

Basic Types · TypeScript

typescript-ninja/typescript-in-definitelyland: TypeScript in Definitelyland ちょっと古い

バージョンごとの変更点は vvakame さんの Qiita が参考になります 「user:vvakame tag:typescript」の検索結果 - Qiita

TypeScript は C#よりもかなり柔軟な型の表現を持ちます。これはもともと動的な JavaScript の表現を可能なかぎりカバーするために、豊富な表現が可能になっています。

  • SubType
  • Union Type
  • Generics
  • 値型(いわゆる存在型とはちょっと違います)

TypeScript コンパイラの機能は、次の 3 つに分類することが出来ます

  • 静的な型のチェッカー
  • 型の除去
  • ES2015 から ES5 への変換

TypeScript ユーザーの知るべきこととして、TypeScript の型は、JS への変換時にはただ取り除かれるだけです。型によってランタイムの処理が変わることはありません。

// before
const x: number = 1;
class C implements Base<K> {
  private foo(): void {
    return;
  }
}
// after
const x = 1;
class C {
  foo() {
    return;
  }
}

とはいっても、ランタイムへの影響が出る例外が 2 つあります。

  • 非標準な enum 機能。実際に値を生成するので、ランタイムに関与します。
  • 非標準な namespace 機能。ES Modules が策定されるより前の機能なので、基本的に使う必要はありません。

標準ライブラリや外部ライブラリなどは ジェネリクスとともに表現されることがあります。

// promise の例
async function fetchFoo(): Promise<string> {
   const res = await fetch('/api/foo')
   return res.text()
}

// react の例
import React from 'react'
export default class MyApp extends React.Component<{a: number}, {b: number}> {
  render() {
    return <div>app</div>
  }
}

エコシステム編

という章を頑張って書こうと思いましたが、もうこのあとは試行錯誤なので、いくつかポインタを置いておくだけにします。

チーム開発でやる場合、社内の強い人を呼んでくるとか、僕みたいなフリーランス呼んでください。誰かが一度書けば、だいたい1年はメンテナンスフリーで回ります。 運用編、あとで書くかも。

off-the-main-thread の時代

off-the-main-thread は今フロントエンドで熱いテーマの一つです。日本語圏では今ひとつ話題になってないので紹介しておきます。

off-the-main-thread の概念の大まかな概要については、Chrome 開発者の nhiroki さんの日本語の記事があるので、こちらを参照してください。

nhiroki.jp

speakerdeck.com

ここまでのあらすじ

従来のウェブブラウザーでは、一つの画面につき一つ割り当てられる、UI スレッドと呼ばれる名前空間で様々な処理を行ってきました。DOMセマンティクスの評価, CSS による rendering / painting、JSのScripting…。もちろん裏側ではブラウザが様々なバックグラウンドサービスに処理を委譲し、スレッドで実行され、その非同期な結果を受け取っているわけですが、少なくともUIスレッドで走るJSからコントロールできる範囲ではシングルスレッドです。

このような状況で、最近のフロントエンドの重厚長大化は、CPUの専有という形で、ユーザーの体験の悪化につながっている、と Chrome デベロッパーの Alex Russell は指摘しています。

The "Developer Experience" Bait-and-Switch | Infrequently Noted

僕個人の意見としては、フロントエンド開発者にとっては拡充される機能郡は一つの福音でありつつも、現実にはIEというブラウザに律速に縛られています。その結果、将来的なゴールは示されているが、現実が追いついていない、ならポリフィルとしての層(babel や webpack)が分厚くなるのは当然だという気がします。

havelog.ayumusato.com

とはいえ、HTML5 と PWA という2つのムーブメントで、ブラウザの機能的な不足は段階的に解消されてきました。また将来に渡って改善していくためのディスカッションの場も、先の十年よりも遥かに増えており、リリースサイクルも主要なブラウザはほぼローリングリリースを採用しており(Safari の不透明感はありつつも)そこまで悲観するものではない、という状況です。実際には、短命なモバイルデバイスのライフサイクルに助けられている形ですが…。あとは、他にEOLなIEが死ぬのを待つだけ。

blog.jxck.io

フロントエンドのリッチ化は ー ときに批判にさらされつつも ー 機能的な不足は解消されつつあり、代わりにパフォーマンスの問題に向き合う時期が来た、というのが自分の認識です。

どう向き合うか

メインスレッドが専有されるとどういう問題が起きるでしょうか?具体的には、 16ms を超えてCPUメインスレッドが busy になると、クリックやスクロールなどが即座に反応しなくなります。いわゆる「もたつき」として認識されます。

off-the-main-thread は、言葉から分かる通り、マルチスレッド化するということより、メインスレッドではない、という部分に重きがあります。つまりこのもたつきを発生させる処理を別スレッドに逃すということです。

実際のソリューションは、既に主要なブラウザに搭載されていたが、今まであまり使われていなかった WebWorker を使おう、ということになります。使われなかった理由は、これらの問題が顕在化していなかったためと、JSでわざわざマルチスレッドを持ち込んでまで物事に向き合いたくない、といったあれそれがあったことでしょう。単に使われなかったので枯れていなかったので使われない、という鶏と卵の問題もあったように思います。

自分が思うに、今では ES Modules と Dynamic Import の導入によって、「ファイル名を指定して起動する」ということへの概念的なジャンプが、やや少なくなったように思います。

// ESM: Dynamic Import
const sub = await import('./sub.js')

// WebWorker
const worker = new Worker('./worker.js')

インラインで worker ロジックを記述できるようにしようぜ、という domenic (Promiseの仕様策定者)の tc39 への proposal もあります。

GitHub - domenic/proposal-blocks: A proposal for a new syntactic construct for serializable blocks of JavaScript code

こんな感じ

const result = await worker({|
  const res = await fetch("people.json");
  const json = await res.json();

  return json[2].firstName;
|});

(これはクロージャへの変数キャプチャのセマンティクスなどが問題になりそうですが…)

結果としてマルチスレッド化すると、マルチスレッドのデザインパターンを導入することもある、ということで、GoogleChromeLabs にはいくらか試験的なモジュールがあります。

github.com

github.com

Comlink はpostMessage のラッパーで、 Clooney はその上で アクターモデルを実装したものです。

UI スレッドでJS ロジックを書くということは、 document や window に触れないJavaScript を書く、ということで、 元は browser / node の抽象化手段だった Universal JavaScript の概念がまた別の役割を背負うようになるのではないでしょうか。

speakerdeck.com

個々のオフザメインスレッドへの向き合い方

Canvas

OffscreenCanvas という提案があります(Chromeで実装・有効化されています)。Canvas の所有権を Worker 側に移譲し、 Worker 側で DOM への操作を行います。

developer.mozilla.org

しれっと、所有権とか言いましたが、これは最近 WebWorker に導入された概念で、ArrayBuffer や ImageBitmap などのオブジェクトを、そのスレッドでの参照を手放すことを条件に別スレッドで使えるようにするインターフェースです。

developer.mozilla.org

(TypeScript に Rust の Borrowing がほしくなりますね…)

AMP

AMP でも Worker 側でJSを実行し、 amp-bind (ampの機能の一つでデータバインド機能) で実質的に任意なJSを実行できるようにしたい、という提案があります。

JS in AMP · Issue #13471 · ampproject/amphtml · GitHub

github.com

React

React v16 のアップデートでは内部がフルスクラッチで書き直され、内部的には非同期な rendering ができるようになっています。が、ユーザー側にはまだ開放されていません。

React v17 では Suspense と呼ばれる機能が導入され、Promise ベースの非同期読み込みや、重い rendering を別フレームに分割することが可能になります。

speakerdeck.com

ただ、これはブラウザ標準と協調して出てきた機能というより、自前でその機構を持とうとしているものに見えますね。WebWorker 使うというより、スレッド専有時間を分割するという発想です。

Vue

Vue の作者 Evan You の最近の記事では、内部の Mutation の適用には requestIdleCallback を使っていくという旨が書かれています。

medium.com

requestIdleCallback は、メインスレッドを専用しない限り裏に積まれたキューが実行される、優先度が低いコールバック登録です。

developer.mozilla.org

これもReactと同じく内部タスクのスレッド専有時間の分割です。

まとめ

話題になっている、といいつつも、実際はChorme周辺から、どうやったらWeb体験を良くできるか?ということから生まれた概念で、そのために WebWorker を使えないか試行錯誤している、という段階です。FMP, FCP, TTIといったメトリクスを改善するために、UIスレッド以外で処理を進めるしかない、といった経緯ですね。

末端の JSプログラマとしては、今後JSのマルチスレッド化が進む潮流が生まれようとしている、という理解をするといいと思います。Flux みたいなメッセージングベースの設計や、node 側から出てきた Universal JavaScript もおそらく有用なんじゃないでしょうか。

tensorflow.js の mnist (数値の画像分類) のコードを読んで勉強した

tfjs の練習がてら、ちょっとやってみた。大学生の頃PRMLを二章ぐらいまで読んでギブアップした程度の知識なので、ほぼ無知。

js.tensorflow.org

間違ってたら教えてほしい…というか特に予習とかせず生半可な知識でやったのでたぶん間違ってる。

入力と出力

  • 32(横)x32(縦)x2(白黒) の画像が入力値(数値が書いてある画像)
  • (なんかいろいろあって) 0 から 9 いずれかの数値に分類する

そのまま入力して関係を見出すのが困難なので、なんやかんやで次元を少なくする

  • 元の画像の入力を8x8 の二次元のベクトル(Tensor)に圧縮
  • 64の入力に対し、いずれかの数値に分類する

そういう仮説が事前にある、というのを受け入れるのが時間かかった。 さすがに画像を放り込むと、なんかすごいディープラーニング様ってやつが出力当ててくれるというものではない。

畳み込み(CNN)

  • ある x, y に対し、その周辺8マスを含む 3x3 の領域で白黒を判定する
  • 2x2 の領域を見て、その中間値で1の値に圧縮する(Pooling)
  • これを二回繰り返す(32*32/4/4=64)

というモデルを表すコードがこれ

function createConvModel() {
  // Create a sequential neural network model. tf.sequential provides an API
  // for creating "stacked" models where the output from one layer is used as
  // the input to the next layer.
  const model = tf.sequential();

  // The first layer of the convolutional neural network plays a dual role:
  // it is both the input layer of the neural network and a layer that performs
  // the first convolution operation on the input. It receives the 28x28 pixels
  // black and white images. This input layer uses 16 filters with a kernel size
  // of 5 pixels each. It uses a simple RELU activation function which pretty
  // much just looks like this: __/
  model.add(
    tf.layers.conv2d({
      inputShape: [IMAGE_H, IMAGE_W, 1],
      kernelSize: 3,
      filters: 16,
      activation: "relu"
    })
  );

  // After the first layer we include a MaxPooling layer. This acts as a sort of
  // downsampling using max values in a region instead of averaging.
  // https://www.quora.com/What-is-max-pooling-in-convolutional-neural-networks
  model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 }));

  // Our third layer is another convolution, this time with 32 filters.
  model.add(
    tf.layers.conv2d({ kernelSize: 3, filters: 32, activation: "relu" })
  );

  // Max pooling again.
  model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 }));

  // Add another conv2d layer.
  model.add(
    tf.layers.conv2d({ kernelSize: 3, filters: 32, activation: "relu" })
  );

  // Now we flatten the output from the 2D filters into a 1D vector to prepare
  // it for input into our last layer. This is common practice when feeding
  // higher dimensional data to a final classification output layer.
  model.add(tf.layers.flatten({}));

  model.add(tf.layers.dense({ units: 64, activation: "relu" }));

  // Our last layer is a dense layer which has 10 output units, one for each
  // output class (i.e. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9). Here the classes actually
  // represent numbers, but it's the same idea if you had classes that
  // represented other entities like dogs and cats (two output classes: 0, 1).
  // We use the softmax function as the activation for the output layer as it
  // creates a probability distribution over our 10 classes so their output
  // values sum to 1.
  model.add(tf.layers.dense({ units: 10, activation: "softmax" }));

  return model;
}

学習

画像とその数値の正解データセットが用意されてるので、それを一つずつ入力する。どの数値っぽいかという出力が出てくる。 出力に対し、正解の教師データと突き合わせて categoricalCrossentropy (ググると Multi-class logloss という名前のが知られている)で、正解との距離を対数で取ったものの和を誤差として用い、 LEARNING_RATE = 0.01 でバックプロパゲーション(誤差伝搬)させる。これでニューラルネット間の係数が修正されていく。

  // optimizer 定義
  model.compile({
    optimizer,
    loss: "categoricalCrossentropy",
    metrics: ["accuracy"]
  });

  // 訓練
  await model.fit(trainData.xs, trainData.labels, {
    batchSize,
    validationSplit,
    epochs: trainEpochs,
    callbacks: {
      onBatchEnd: async (batch, logs) => {
        trainBatchCount++;
        ui.logStatus(
          `Training... (` +
            `${((trainBatchCount / totalNumBatches) * 100).toFixed(1)}%` +
            ` complete). To stop training, refresh or close page.`
        );
        ui.plotLoss(trainBatchCount, logs.loss, "train");
        ui.plotAccuracy(trainBatchCount, logs.acc, "train");
        await tf.nextFrame();
      },
      onEpochEnd: async (epoch, logs) => {
        valAcc = logs.val_acc;
        ui.plotLoss(trainBatchCount, logs.val_loss, "validation");
        ui.plotAccuracy(trainBatchCount, logs.val_acc, "validation");
        await tf.nextFrame();
      }
    }
  });

  // 訓練されたモデルを使う
  const testResult = model.evaluate(testData.xs, testData.labels);

感想

CNN って想像していたニューラルネットっぽくないというか、これ単にヒューリスティックだった下処理に名前が付いてるやつでは?という気持ちになった。

なんか頑張って読むことは出来たが、自分で作れといわれてたら無理なので、素振りする必要がありそう。

SWSRSSR という1フレーム技

Service Worker Side React Server Side Rendering

ServiceWorker内でBabelを駆使して、JavaScriptをビルドする - ログミーTech(テック)

ってスライドの中で、React の SSR を Service Worker の中でやれば、SEO は死んじゃうけど First Meaningful Paint 最適化できるよねーっていう話をしました。

というわけで実装してみました。すごく単純な React + Redux の Counter です。

ここで試せる https://epic-cray-a9cff8.netlify.com

実装コード https://github.com/mizchi-sandbox/swsrssr

DevTools 見る限り、 FMP が 8ms で返ってます。TTI (JSが動くようになる時間) は JSの評価が終わる 150ms ぐらい。 ただ、ネットワーク全く使わないかわりに、処理速度が完全にCPU依存なので、モバイルだとここまではならないとは思います。

これはブラウザの標準的なフレームレートの16ms未満なので、リロードしたことをほぼ認識できない速度です。

この路線がありか、まだわからないけど、夢がありますね。やはり SW 用の express emulator 作ったほうがいい気がする。