雨は夜更け過ぎにコミットログへ変わるクソアプリを作った

この記事はクソアプリ2 Advent Calendar 2019 - Qiitaの24日目の記事です。

f:id:hogesuke_1:20191223234603p:plain

作ったクソアプリ

christmas-eve.hogesuke.net

「雨は夜更け過ぎにコミットログに変わる」クソアプリを作りました。

聖夜に空を見上げるとコミットログがふわふわと降ってくる。

あぁ今年はこんなコードを書いたっけなぁと思いを馳せながら眺める光景はきっと美しく、荒んだ心を癒やしてくれるに違いないと思って作りましたがそんなことはありませんでした。

おわり

これは何か

GitHub APIを通してリポジトリのコミットログを取得し、そのコミットログを雪とともに降らせるアプリです。

f:id:hogesuke_1:20191223234722p:plain

右上にリポジトリ名を入力してEnterすると、好きなリポジトリのコミットログを降らせることができます(Privateリポジトリは不可)。

試してみてくれると喜びます。

なにで作ったか

three.jsを使ったWebGLのアプリです。

PCではマウス操作で、AndroidiOSではジャイロを使って全方位を見渡せるようになっています。 three.jsで提供されているクラスやライブラリを使って、今回のアプリは簡単に実装可能でした。

実際のところ3Dに関する知識やWebGLに関する低レイヤーな部分についてはほとんど分かっていないのですが、それでも何となく動くものが作れるというのはthree.jsのすごさでしょう。

コードは以下にあります。

github.com

苦労した点 その1

とにかくロードに時間がかかる

three.jsはminifyしたものでも約600KBのファイルサイズです。
その他の今回使用したライブラリを含めたbundleしたJavaScriptファイルは、nginxでgzipして配信しても約500KBとなっています。

webpackのTree Shakingで使用しないコードを削ぎ落とせないか考えたのですが、現時点においてはTree Shakingに対応できていないようでした。

参考: https://discourse.threejs.org/t/three-js-file-size-when-importing-via-npm-and-bundling-with-webpack/8904/2

three.jsの本体だけでなく、アセットもファイルサイズが大きくなりがちです。

このアプリでは背景に360°の全天球画像を使用しています。
背景を画面いっぱいに表示するため、それなりの解像度が必要かつ、Retinaでも表示することを考えると解像度を落としづらいという問題があります。
そのため、背景画像だけで485KBにもなってしまいました。

fontもかなりのファイルサイズになります。
three.jsでtextをレンダリングするには、fontデータをJSON形式に変換したものが必要です(これはfacetype.jsで生成できます)。
欧文フォントであれば数十キロバイト程度ですが、和文フォントとなると数MB〜十数MBにもなります。 今回使用した和文フォントのJSONは、gzipで配信してもなお2MBの巨大ファイルとなってしまいました。

これらのファイルをダウンロードするにはかなりの時間を要するため、なんらかのごまかしが必要です。

ローディング時間をごまかす

このアプリで使用するすべてのリソースサイズを合計すると3MBにもなります。
環境にもよりますが、すべてのロード完了までには結構な時間がかかってしまうでしょう。
特にWifiに接続していないモバイル環境では致命的です。

そこで、いくつかのステップでユーザーに離脱されないようにごまかしを施しました。

script要素のdefer属性

まず、メインのJavaScriptファイルとなるbundle.jsのscript要素にdefer属性を付与し、500KBのbundle.jsのロード完了まで「何も表示されない」という最悪の状況を回避します。

よく意味が分からないテキストを表示

つづいて、よく意味が分からないテキストを表示し、時間稼ぎをします。
「こいつは何をいっているんだ?」という疑問でほんの少し時間を稼げたら幸いです。
「雨は夜更け過ぎにコミットログへ変わる」がそれです。

ローディングステータスの表示

それだけでは時間が稼ぎきれないので、ローディングステータスを表示します。
フリーズしている訳ではなく、確実にロードが進行していることをユーザーに伝えます。

単純なスピーナーでは進行状況を伝えられないので、全体のアセット数とロード完了したアセット数をテキストで表示しています。

f:id:hogesuke_1:20191224000659p:plain

three.jsで提供されているLoadingManagerを使用すると、アセットのローディング状況をイベントで取得可能です。

参考: https://threejs.org/docs/#api/en/loaders/managers/LoadingManager

とりあえずサンプルリポジトリのコミットログを降らせる

ひとまずアプリを動かせる最低限のローディングが完了したら、サンプルとなる自前のリポジトリのコミットログを降らせてしまいます。

このサンプルを動かしている裏では、ファイルサイズの大きい和文フォントのJSONをロードしており、ユーザーが任意のリポジトリを指定した際に日本語を表示できるように備えます。

サンプルリポジトリのコミットメッセージには英字のみを使用し、和文フォントを必要としないためサイズの小さい欧文フォントでまかなっています。

以上

このような感じでごまかしている気でいますが、1Mbps以下の通信速度のような環境ではロード完了までかなりの忍耐が必要になります。

昼休みなどの混み合う時間帯は厳しいかもしれません。

苦労した点 その2

テキストのオブジェクト生成処理が重い

テキストのオブジェクト(TextGeometry)の生成処理がとても重く、かなりの時間を要します。

100文字前後のテキストで百数十ミリ秒の処理時間がかかっていました。
これはMacBookPro 2019で実行した結果なので、スマホなどの非力なデバイスだとさらに遅くなります。

このアプリでは最大100コミットログを表示しているため、すべてのTextGeometryを生成するのに数秒〜数十秒かかります。

JavaScriptはシングルスレッドで動作するため、TextGeometryの生成処理が動いている間はrequestAnimationFrameの呼び出しがされずフリーズしてしまいます。

WebWorkerに処理を委譲しようとするも失敗

これを解決するため、TextGeometryの生成処理をWebWorkerに委譲できないか試してみましたが失敗しました。

WebWorkerで生成したオブジェクトをメッセージで送受信する際に、そのオブジェクトが持つ関数が失われてしまったためです。
これはメッセージをJSONシリアライズする際に、関数を省略してしまう仕様によるものでした。

関数を残したままJSON化させる方法もあります。
参考: https://qiita.com/suetake/items/52ec9d22e978ceb3111c

しかし、詳細は忘れてしまいましたが、この方法では別の問題が発生しうまくいきませんでした。

別の方法として、Object3DクラスのtoJSONで取得したJSONをWebWorkerから送信し、受信側でJSONLoaderを用いてdeserializeする方法を試してみましたが、TextGeometryには対応しておらずこれも失敗に終わりました。

下はうまくいかなかったコードの例です。

const geo = new THREE.TextGeometry('hogehoge'); 
const serialized = geo.toJSON();
const jsonLoader = new THREE.JSONLoader();
const parsed = jsonLoader.parse(serialized); // ここのパースで「TextGeometryは未対応」というエラーとなる
const deserialized = parsed.geometry;

TextGeometryをTextBufferGeometryに変更

その後、調べているとTextBufferGeometryというものの存在を知りました。

そもそも、GeometryはBufferGeometryを扱いやすくしたものらしいのですが、処理コスト的はBufferGeometryに劣るということでした(よく分かっていない)。

TextGeometryをTextBufferGeometryに変えてみるとそれだけで、おおよそ5分の1の処理時間になりました。
根本的な解決にはなっていませんが、ひとまずパフォーマンスの問題は軽減されたので妥協しています。

three.jsを初めて触ってハマった点

いくつか、はちゃめちゃにハマった点があるので、これからthree.js(WebGL)をやろうとする方が同じ轍を踏まないように記してみます。

スマホでジャイロが機能しない

これは、httpsでないページではDeviceOrientationEvent(ジャイロ)の検知が許可されないという問題によるものでした。

Let's encryptを使うなどしてhttps化する必要があります。

Mobile Safariでジャイロが機能しない

Mobile Safariはジャイロがデフォルトで無効になっているため、iOSの設定からジャイロ機能をONにする必要があります。

参考: https://tips.spacely.co.jp/ios12-2_safari_gyro/

サンプルコードが動かない

使っているthree.jsのバージョンが異なることによって動かない場合がありました。

three.jsはセマンティックバージョニングを採用していないため分かりづらいのですが、バージョンによっては破壊的変更が含まれます。

サンプルコードで使用しているバージョンを記載してくれていることが多いので、それを確認するようにしたほうが良さそうです。

特定の端末でのみ動作しない

これはthree.jsは関係ない話ですが…

webpackのdevtoolevalにしてビルドしたjsをiPhoneで動作確認したところ動作しない事象に遭遇しました。

devtool'inline-cheap-source-mapに変更したところ問題が解消されたため、おそらく特定のブラウザの特定のバージョンにおいてevalでの解釈に問題があり動作しなかったと思われます。

特定の端末で不可解な事象に陥った場合にはwebpackを疑ってみるのも良いかもしれません。

おわりに

素敵なクリスマス・イブを!! 🎄🎁🥂🎅🏻✨

christmas-eve.hogesuke.net