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

二世の呪い

僕はエンジニアで、このブログで書くことは、そういうテーマを期待されていることを知っている。それ以外はノイズだから、あんまりやらないでほしい、とも。

でもこれは自分のアイデンティティの根幹に関わることで、そういう前提で、一部で話題になってたこの動画を見た。幸福の科学大川総裁の息子の、幸福の科学との断絶宣言。

www.youtube.com

happy-science.jp

エンタメの文脈でそれはどうなんだと思うところはあれど、内容自体は非常に思うところがあった。


8歳ぐらいまで、家の宗教に疑問を持つことはなかった。幼稚園までは、人に隠れて食前の祈りを捧げていたと思う。それが褒められると知っていたから。

ティーンエージャーの頃、自分は怒りに支配されていた。自分の家の異常さを客観視できるようになり、その異常さを許せなくなった。自派以外を否定する排他的な教義、一時期採用された一夫多妻、そしてその終末論は、オウム後の広まったカルトの概念と認識されるのに十分だったと思う。学校でも隠していたが、時折聞きつけた人がいた。そのたびに、誰にも言わないでくれ、と頼み込んだ。

最初はただの反抗期だったかもしれない。だが、そこに強い理由付けが行われた結果、それらは親子の間で一生消えない溝となった。

愛されては、いたと思う。ただし、それは自分が納得いく形ではなかった。必ず神の愛という概念を経由して行われたように思う。

「聖書にそうあるから」は、自分には思考停止にしか見えなかった。それに対し、射に構えても良いことはないぞ、お前は祝福を受けているから、他の人と違うのは当然だから、と説法を受ける。お前は、それを受け入れられない捻くれ者だと。

日曜日に教会へ通うのは、少なからず精神的な暴力の形をとった。ぶたれたりとかは、しなかったけれど、強い言葉で問い詰められ、圧迫された。

教会に通うのは精神的な訓練の側面がある、というのが父の認識だったと思う。教会に行かないのは、単なる「怠惰」であり、教会に連れて行くのは教育的な指導である、という体裁をとる。

自分も、自分の怠惰さから教会に行きたくなかったのを否定できない。そりゃそうだ。日曜日は友人と遊びたいに決まってる。

教会へいき、得るものがあれば、また違ったかもしれないが、かといって自分が教会にいって得られるものは、何もかも無意味に見えた。聖書の警句や解釈。教団初期の歴史。社会的にどんな立派な活動をしたか。伝道活動の成果共有…


高校生の頃、「ショーシャンクの空に」の映画を見た。そのときの感想は、 「無実の罪で収監され、そこで一生の友人を得たといっても、囚われたこと自体を肯定できるだろうか?」

これがしばらく人生のテーマにだったと思う。確かに教会の中で友人も得た。しかしそれは、交友関係がそこに限定されていたからだ。また、自分の言語能力は、子供の頃の聖書の輪読の習慣に由来すると思う。だからといって、それを肯定することはできない。プログラミングを始める前提のコンピューターリテラシーは、宗教的な倫理感から付与されたPCへのアクセスコントロールを、様々なテクニックで外す過程で身につけたものだった。

そして今、その怒りに人生を支配されていたことを、反省している。自分の人生の一部を無駄にされたことなど、とるに足らないことなのだ。とるに足らないことへの怒りに振り回されることこそ、人生を最もムダにすることだ。

だから、限界までなかったことにする。風化させることしかない。ひたすら距離をとって、相容れないことを表明し続け、「お休み会員」の復帰をお願いする宣教師に、次来たら通報しますよ、と告げる。そうやって生きるしかない。

できれば、こう表明することで、勇気を持てなかった人のための一歩になれれば、と思う。

今はそう思ってます。

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 作ったほうがいい気がする。