ウェブアプリのパフォーマンスに対する共通の捉え方を持つ
最近、一緒に仕事をしている企画者に「ブラウザAでページBを開いたら重かったです」と言われたことがあって、そのときに思ったこと。
「重かったです」と言ってくるということは何か良からぬことが起きている気がするけれど、それだけだと何が起きているのか、何が問題なのかがよく分からないので、以下のような質問で原因で探っていく。
- ページの表示が遅いのか
- 操作(スクロールやクリック)の反応が悪いのか
- アニメーションが滑らかでないのか
このような質問をしていて思ったのは、ウェブアプリのパフォーマンスに対する捉え方を啓蒙したり、互いに共通の捉え方を持つといいのかもしれない、ということ。例えば、上記の質問は、自分が知っている RAIL というパフォーマンスモデルに基いて行った質問だった。RAIL はウェブアプリのパフォーマンスをユーザーの体験から4つの側面で捉えたモデルで具体的な内容は RAIL モデルでパフォーマンスを計測する | Web | Google Developers の解説が詳しい。
一度、こういったパフォーマンスモデルを共有しておけば、以後は冒頭のようなやりとりがスムーズにおこなうことができる。また、エンジニア同士でも「とりあえず改善しました」系の PullRequest がなくなり、RAIL のどの側面でコードを具体的にどのように修正したかが明らかになる。それにより、コードレビューが行いやすくなったり、なんとなくな改善を手当たり次第におこなうことが無くなるのではないだろうか。
date-fns に乗り換えて、スクリプトのファイルサイズを削減した
題の通り。
Browserify を使ったプロジェクトでファイルサイズを大きくしているライブラリを探す - kitak.blog で moment がけっこうな割合を占めているということを書いたのだけど、少し前に moment から date-fns に乗り換えて、ファイルサイズを削減できた。
moment がオブジェクトから日付操作などの様々なメソッドを呼び出すことができるのに対し、date-fns は 日付に関係するユーティリティ関数が集まったライブラリといえる。
例えば、date-fns でふたつの日付が同じ週か調べる場合は以下のようになる。
import isSameWeek from 'date-fns/is_same_week' const result = isSameWeek( new Date(2014, 7, 31), new Date(2014, 8, 4) )
日付操作の関数を必要に応じてインポートするので、Browserify, Rollup, Webpack のようなモジュールバンドラを利用した場合に、ブラウザの JavaScript エンジンで実行される(本当に必要な)コードだけバンドルされる。 自分が担当しているプロジェクトだと moment をバンドルした場合と比べて、900k 程度削減することができた。
もし、moment を使っていて、ほんの一部のメソッドしか使用していない、かつファイルサイズを削減することに意義のあるプロジェクトなのであれば、date-fns への乗り換えを考えてみてもいいかもしれない。
また、自分でライブラリを作る際に、(moment のようにオブジェクトのI/Fにする方が適切なケースもあるかもしれないが) date-fns を参考にして、モジュールバンドラフレンドリーなライブラリにできないか考えてみてもいいかもしれない。関数毎にファイルを分割したり、Tree Shaking が効くように ES2015 の Named export でモジュールを定義する、等など。
誰が音楽をタダにした? を読んだ
読んだ。
mp3 が世の中に普及して音楽業界を変えるに至った話をストーリー仕立てでまとめた一冊。
mp3 を生み出した研究者、音楽業界、「シーン」と呼ばれるインターネットの地下コミュニティ、この3つの糸が絡み合いながら、物語が進行していく。自分にとって、mp3 は物心がついた頃から当たり前の存在なのだが、正直、それが生まれた経緯であったり、世の中に普及した経緯は全く知らなかったので興味深く読めた。
誰が音楽をタダにした? 巨大産業をぶっ潰した男たち【無料拡大お試し版】 (早川書房)
- 作者: スティーヴンウィット
- 出版社/メーカー: 早川書房
- 発売日: 2016/09/07
- メディア: Kindle版
- この商品を含むブログを見る
ルパート・サンダース監督 Ghost in the Shell を観た
今日の午前中に、渋谷の TOHO シネマズで観た。
都市や街並みの映像が良かった。単純にきれいだし、アニメ映画版、イノセンスと続く、ブレードランナーの DNA の系譜を継いでいるように感じた。 また、ひとつひとつのシーンの撮り方や、各所に散りばめられた小道具から、アニメ映画版やイノセンスに対するリスペクトも感じた。ガブリエルかわいいよ。
一方、映画の時間の長さ上、致し方ない気がするのだけど、公安9課のポジションがいまいちパッとしなかったのと、ひとつひとつの場面が明快で、全体としてきれいにまとまりすぎていて物足りなさを感じてしまった。イノセンスのように難解でなくていいのだけど、Stand Alone Complex の Stand Alone エピソードのような見終わった後の余韻、Complex エピソードのようなもう一度見たくなる程度の難しさが脚本に欲しかった。映像の質やスカーレット・ヨハンソンが演じる少佐の演技が良かっただけにそこがもったいないな、という気持ちになってしまった。
Vue.js v2 で揮発性の現象を扱う(ダイアログ等を実装する)
同僚に題の相談を受けていたんですが、そのときに「あんまりこの話、ググっても見かけないね」という話になったのでブログに書いておきます。
SPA でダイアログを実装する機会があると思うんですが、よくある事前にコンポーネントツリーのルート直下にダイアログのコンポーネントを入れておく実装だと、マークアップやスタイルシートの事情でその位置に入れるのが難しかったり、テンプレートにダイアログのコンポーネントがズラズラ並んでメンテナンスするのが大変になってきます。ダイアログのような、一時的にそのときだけ必要になる UI・コンポーネントは他の GUI プラットフォームでは「揮発性の現象」と呼ばれているようです。こういった UI・コンポーネントは、それが実際に画面に表示される時間と合わせて、必要になったタイミングでコンポーネントツリーにコンポーネントのインスタンスが追加され、役目を終えたら削除されるのが理想です。
Vue.js では、以下で取り上げる API を利用することで上に述べた内容を実現することができます。
まず、動的にコンポーネントツリーにコンポーネントのインスタンスを追加するところから。次のコードは、コンポーネントのオプションの一部で、ボタンが押されたときに実行されるメソッドが定義されています。
{ methods: { openDialog: function () { let fooDialog = new FooDialog().$mount(); this.$el.appendChild(fooDialog.$el); } } }
エントリとなる要素を別で用意しておいて、マウントさせることもできます。( refs: https://jp.vuejs.org/v2/api/?#vm-mount )。
new FooDialog().$mount(el);
ダイアログで表示するデータの設定は、props で渡すか( https://jp.vuejs.org/v2/api/#propsData )、ダイアログを開くためのメソッドをダイアログのコンポーネントに定義して、その呼び出しの引数で渡す等、やり方は色々あります。 生成したコンポーネントインスタンスがコンポーネントツリーに追加される = ダイアログの表示と考えてよければ前者で、表示のタイミングをもう少し制御したい場合は後者、というようにアプリケーションに応じて都度判断する必要があります。
コンポーネントインスタンス生成時の propsData オプションは、コンポーネントのユニットテストのために用意されたオプションのようなので、本来想定されているケースとは異なる使い方かもしれません。バージョンアップのときに書き換えが発生する可能性があることを理解した上で使うのが良さそうです。
ダイアログの操作のハンドリングは、Vue.js のイベントインターフェイス( refs: https://jp.vuejs.org/v2/guide/components.html#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%81%A8%E3%81%AE-v-on-%E3%81%AE%E4%BD%BF%E7%94%A8 )を使うか、props にコールバック用の関数を渡して操作が完了したタイミングで呼び出してもらう( https://jp.vuejs.org/v2/api/#propsData )等、これもやり方が色々あります。これに関しても、アプリケーションに応じて適切だと思う方を選べばよいでしょう(悩ましい場合は、おそらくどちらでも問題ないので、好きな方を選べばよいでしょう)。
Vue.js のイベントインターフェイスを利用する例
// コンポーネントインスタンスを生成した親コンポーネントインスタンス fooDialog.$on('click', (event) => { }); // コンポーネントインスタンス内 this.$emit('click', event);
props にコールバックを渡す例
// コンポーネントインスタンスを生成した親コンポーネントインスタンス let fooDialog = new FooDialog({ propsData: { onClick: function (event) {} } }); // コンポーネントインスタンス内 this.$props.onClick(event);
ダイアログの操作が完了したら、$destroy ( refs: https://jp.vuejs.org/v2/api/#vm-destroy ) で破棄して、コンポーネントツリーから除きます。
// コンポーネントインスタンスを生成した親コンポーネントインスタンス fooDialog.$destroy(); fooDialog = null;
といったかんじで、Vue.js で揮発性の現象を扱うための API を見てきました。ダイアログ等を実装するときの参考にしていただけたらと思います。
逃げるは恥だが役に立つ ( 9 ) を読んだ
最終巻を読んだ。
- 作者: 海野つなみ
- 出版社/メーカー: 講談社
- 発売日: 2017/03/13
- メディア: Kindle版
- この商品を含むブログ (3件) を見る
最終巻の見どころは、百合ちゃんがポジティブモンスターに「呪い」について説く場面だろう。
この「呪い」は個人の行動や考え方を縛り付けている何かを指す。思い返せば、逃げ恥の登場人物は、それぞれが何かの呪いにかかっていた。平匡は女性と親しくなることを避けようとする呪い、みくりは自分を小賢しいと貶める呪い… などなど。
それぞれの呪いが、ライフスタイルを変えて、勇気を持って最初の一歩を踏み出すことで解けていく。そんな物語が逃げ恥だったのではないか、という気持ちのする終わり方だった。
BroadcastChannel API を使ってみた
BroadcastChannel API を使ってみた。
同一オリジンのタブ・ウィンドウ・フレーム間でデータを送受信できる API。API の詳しい説明は BroadcastChannel API: A Message Bus for the Web | Web | Google Developers に譲ります。コードは、パッと見、Cross-Document Messaging とほぼ同じですね。
サポートしているブラウザは、Blink 系のブラウザ( Chrome, Opera )と Firefox のみ( http://caniuse.com/#feat=broadcastchannel ) なのでまだ実戦では使っていないのですが、先日、個人的に便利に使える場面がありました。
先日、プレゼンをする機会があったんですが、プレゼンに使ったツールが https://github.com/ahomu/Talkie というブラウザで動くツールで(このツール、Markdown で書けて、デフォルトでけっこう良い感じに出力してくれるのでオススメです)、そのとき、諸々の事情で手元のPCの画面のブラウザウィンドウで表示しているスライドの内容を、スクリーンで映している別のブラウザウィンドウのスライドに共有・同期する必要がありました。始めは WebSocket を使おうかな、と思っていたのですが、「いや、そういえば BroadcastChannel API ってのがあったよな。まさしくこういうときに使えるのでは」と思い、試しました。
手元で開いているスライドでは、以下のスクリプトを実行して、Talkie でスライドを切り替わったときに現在のページ番号をメッセージとして送ります。
var channel = new BroadcastChannel('talkie'); talkie.changed.subscribe(function(current) { channel.postMessage(current.getAttribute('data-page')); });
スクリーンで映しているスライドでは、以下のスクリプトを実行して、メッセージを受け取り、表示するスライドを同期させます。
window.onmessage = function (e) { // 実戦で使う場合は、どの Origin から送られたデータか等チェックしなければいけない talkie.jump$.next(e.data); };
こんなかんじでやりたいことが簡単に実現できました。
実戦では、以下のような仕様を実現するために使えそうです。
- 動画や音声を再生するサービスで、再生中に別のウィンドウで開いて再生を開始したら、自動で停止する (YouTube や SoundCloud がやっている)
- 商品にお気に入りをつけることができるサービスで、お気に入りをつけた際に他のウィンドウ・タブに最新の状態を送信する。あるいは、最新の状態に更新するよう促す
こういった仕様を BroadcastChannel API を使わず実現するには、WebSocket を使う、あるいはブラウザだけで完結させるには、LocalStorage や cookie のようなストレージを使って、あらかじめストレージの内容を監視させ、通知を行う際にストレージにデータを書き込むといった実装を行う必要があります。自分も過去にやったことがありますが、実装が面倒な上に監視を setInterval/setTimeout によるポーリングで実現するので、パフォーマンス上の懸念もあります。 BroadcastChannel API でこういった仕様が簡単に実装できるようになるのは、ありがたいですね。