kitak blog

Kみたいなエンジニアになりたいブログ

かくかくしかじか を読んだ

読んだ。

東京タラレバ娘海月姫東村アキコの伝記的な漫画。高校時代から始まり、漫画家としてデビューするまでの絵の「先生」との交流を描く。

作者の中身を全て曝け出すような心理描写と「先生」が繰り返し発する「描け」というメッセージに心が揺さぶられた。

Vue.js のリストレンダリング( Virtual DOM 実装 )と相性の悪いデータの変更

この間、現場で直面したやつの個人的なメモです。対象のバージョンはこの記事を書いている時の最新バージョン v2.3.0。

以下のようなリストレンダリングをおこなうコンポーネントがあるとして、

<script>
export default {
  name: 'list',
  data() {
    return {
      items: [1, 2, 3, 4, 5]
    };
  }
}
</script>

<template>
  <ul v-for="item in items" :key="item">
    <li>{{ item }}</li>
  </ul>
</template>

items のサイズの上限を 5 として、新しいデータを追加したい場合は古いデータを削除することとする。
例えば、items [1, 2, 3, 4, 5] に 6 と 7 を追加する場合、items は [3, 4, 5, 6, 7] になるが、Vue.js の Virtual DOM 実装は以下のような diff/patch を適用する。

  • 新しい vdom の先頭の 3 が古い vdom にないか探し、見つかったので 3 のアイテムの DOM 要素をリストの先頭に移動させる(ここで key 属性の指定が効いている)。4, 5 も同様。
  • 6 は無いので、新しく DOM 要素を生成して追加する。7 も同様。
  • ここまでで 3, 4, 5, 6, 7, 1, 2 と要素が並んでいる。1 の要素は不要なので削除する。2 も同様。

以上で、新しいデータが Real DOM に反映された。このロジックは updateChildren 関数 (vue/patch.js at v2.3.0 · vuejs/vue · GitHub) に書かれている。上から下に読んでいくと分かりづらいので、上記のようなサンプルを用意し、DOM に Breakpoint を設定して実際にコードを動かしながら処理を追っていくと読みやすい。
本来であれば、1, 2 をそれぞれ変更して末尾に移動させるのが最も効率が良いはずだが、リストの最大サイズ + 変更のあった件数分 の DOM 操作が必要になる。リストの最大サイズが増えれば増えるほど、時間がかかる。

自分の場合は、最大サイズがそれなりに多かったのと、データの更新頻度が多く、Script の処理が増えて、fps がかなり低下してしまった(Timeline で調べたら、updateChildren 関数が Script 処理の 80% 以上を占めていた)。結局、この部分は直接 DOM 操作をおこなうことにした。Virtual DOM は、大体のケースである程度うまくいくが、このように極端なケースではうまくいかない。

ちなみに、フォームで何かを入力してボタンを押したらデータを1件追加する、あるいはリストから1件アイテムを削除するといったよくあるケースの場合は DOM 操作が 1 回で済むようになっている。

ウェブアプリのパフォーマンスに対する共通の捉え方を持つ

最近、一緒に仕事をしている企画者に「ブラウザ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 は物心がついた頃から当たり前の存在なのだが、正直、それが生まれた経緯であったり、世の中に普及した経緯は全く知らなかったので興味深く読めた。

ルパート・サンダース監督 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 を見てきました。ダイアログ等を実装するときの参考にしていただけたらと思います。