読者です 読者をやめる 読者になる 読者になる

k0kubun's blog

railsへの執着はもはや煩悩の域であり、開発者一同は瞑想したほうがいいと思います。

SSEを使ってHTMLエスケープを高速化してみた

高速なHTMLエスケープをするライブラリを作った

ある日HTMLエスケープを速くしたくなって、hescapeというライブラリを作った。

github.com

とにかく速いHTMLエスケープがしたい

Railsアプリのビューのレンダリングにおいて、CGI.escapeHTMLを高速化*1することでRailsのデフォルトのテンプレートエンジンが大きく高速化されたり*2、GitHubでもHTMLエスケープが全体のパフォーマンスに影響が大きかった事例もある*3など、常に自動でHTMLエスケープが行なわれるRailsの環境ではHTMLエスケープの速度が割と大きな意味を持っている。

従って、Hamlitの最速性を維持するためにHTMLエスケープのパフォーマンスを極めておきたかった。

vmg/houdini を倒したい

前述したGitHubの人が既にhoudiniというかなり速いエスケープライブラリを作っていて、escape_utilsというgemを使うとHamlやSlimでそれが使え、HamlitやFamlはデフォルトでそれが使われるようになっている。Rubyにおいてこれより速くHTMLエスケープを行う手段を知らないので、これを越えることが目標になる。

Streaming SIMD Extensions

とはいえhoudiniはよくできているので、一緒に協力してテンプレートエンジンを作っている*4 id:eagletmt さんと去年「(HTMLエスケープは)まああれより速くしようがないですよね」みたいな話をしたのだけど、この前Tokyo RubyKaigi 11の懇親会で id:nurse さんが「HTMLエスケープはSSE使えば速くできるよ」とおっしゃっていた。

h2oのHTTP/1パーサに使われているpicohttpparserもSSE命令(PCMPESTRI)で速くなっていて *5、ループで1文字ずつチェックしていたのが1命令で16文字同時にチェックされるようになっている。HTMLエスケープでも、エスケープすべき文字がない時特に速くなりそうな感じがしたので、試してみることにした。

一人チューニング大会

SSEを使う以前にそもそもCで速いコードを書くのがあまり得意ではないので、事前にテストとベンチマークを用意して、ひたすらベンチマークの結果をよくする、という一人ISUCONみたいなことをやっていた。結構面白いので、やってみたい人は以下を読まずにHTMLエスケープ(CGI.escapeHTMLコンパチのもの)を実装してみると良いかもしれない。

ベンチマークには、一切エスケープをやらないもの全部エスケープするもの、あとライバルのhoudiniが使っているものを用意し、CIでhoudiniの何倍速いかというのを出していた*6。以下は実装ログとwercker上でのベンチマークの結果(houdiniより何倍速いか)の推移である。

コミット 概要 no escape all escape houdini bench
Implement HTML escape 素直に実装した奴。遅い。 0.04x 0.43x 0.08x
Reduce allocation times エスケープが発生した時のみreallocするようにした 0.53x 0.43x 0.85x
Lazily copy unescaped characters 非エスケープ文字はエスケープが発生した時にまとめてコピーするようにした 0.62x 0.35x 0.93x
Skip allocation when nothing is escaped エスケープが行なわれない時はmallocが起きないようにした 1.13x 0.34x 0.91x
Increase allocation size by 1.5 houdiniのパクリ。2回目以降のエスケープ時、メモリが足りない時に必要分の1.5倍メモリを確保している。いいのか…と思うけど対houdiniなら公平。 1.12x 0.61x 1.01x
Optimized strlen of escaped string strlen対象が限られてるのを利用してstrlenを四則演算に変えている。やんちゃチューニング感ある。 1.08x 0.99x 1.11x
Change ensure_allocated to be static ある関数をstaticに変えただけ 1.08x 1.16x 1.13x
Skip non-escaped characters fast この辺で語られてるテクニック(をやっているつもり)。エスケープしない時に局所的にループを回している。 1.17x 1.16x 1.14x
Optimize by pcmpestri intrinsics ここでSSEを使った。全部エスケープだと遅くなるが、エスケープがない場合は本当に速い。 7.70x 追記 0.83x 1.63x

結果

普通のWebアプリのほとんどのケースでは実際のエスケープは走らないわけだけど、そのケースにおいて PCMPESTRIを使っただけで6〜7倍になった(下記の追記を参照)。ただ、あまり何も考えないで作った非現実的なベンチなので、参考にするなら例えばURLくらいの長さの文字列のエスケープを見た方が良いと思う。

本当はHamlitにいれようと思ってたけど、保守性が下がる割に全てのケースで速くなるわけではない*7のと、ビルドの時に考えることが増えるので、HamlitでSSEを使うのは一旦見送ることにした。

08/16 23:35 追記

shinhさんににご指摘いただいた致命的なバグを直したことによりエスケープなしのケースが 7.29x→2.78x くらいに変化している。いろいろ指摘をいただいたので直している最中で、それが終わってみないと実際どのくらいの速さになるものなのか不明。

感想

最初どうやって使うのかが全然わからず難しいなと思ったけど、その難しさに見合う結果は得られたのでよかった。picohttpparserのコードを参考にしてたのでSSE4を使っているけれど、後で後継のAVXも試したい。

*1:https://github.com/ruby/ruby/pull/1164

*2:http://k0kubun.hatenablog.com/entry/2016/05/29/215851

*3:https://github.com/blog/1475-escape-velocity

*4:http://k0kubun.hatenablog.com/entry/2015/12/12/000037

*5:http://blog.kazuhooku.com/2014/12/improving-parser-performance-using-sse.html

*6:10%エスケープというのも見てたけど、houdiniのベンチとあんま変わらんのと横幅の関係でこの記事からはカット

*7:これもうちょっとなんとかならないかな…

pure mrubyで実装されたItamae「itamae-mruby」を作った

itamae-goを作り直してitamae-mrubyを作った

先週Goからmrubyを使ってRubyなしでItamaeレシピを実行できる「itamae-go」を作ったんだけど、全く同じコンセプトの、RubyなしでItamaeレシピを実行できる「itamae-mruby」を作った。

github.com

itamae-goの問題点

mrubyは組み込み言語だしこれは本来想定された使い方であり、go-mrubyの実用的な例として普通に作ってよかったと思っているけど、ことItamaeを実装することに関しては以下のような問題があった。

  • レシピを読む部分以外をGoで実装していたので、specinfraのコードの移植に手間がかかる
  • 主にstandaloneなバイナリを吐く目的にGoが使われているが、mruby-cliでもできるのでGoを使っているメリットがそれほどなく、2つの言語をブリッジするコードを書く労力に見合わない

僕としてはRubyなしで動くItamaeは環境構築の用途にやっぱり欲しいし、ちゃんとメンテし続けたいので保守がしやすいように作り直した。

mruby-cliとは

Goと違ってmruby自体には自分が書いたスクリプトをstandaloneなバイナリにする手段はない(多分)。そこで、mrubyのC APIを使ってmrubyで書いたコードの__main__メソッドにargvを渡して呼び出すCのコードと、それをクロスコンパイルするDocker環境を提供しているのがmruby-cliである。 なので、mruby-cliを使えばRubyの存在に依存しないバイナリをRubyで書くことができる。

itamae-mrubyの現状

  • 現時点で基本的な機能はitamae-goのスーパーセット
    • mruby-yamlのクロスコンパイルがうまくいかないので一旦--node-yamlは外している
  • OSX, Arch, Debian, Ubuntuに加えCentOS, Gentooとかもサポートするようにした
  • 多分動くけどまだちゃんと検証できてない

itamae-mrubyの設計

ryotaraiさんがmrubyで実装した奴をItamae 2にしようとか言ってたので微妙に手間をかけて作っている。まあそんな簡単には置き替えられないと思うけど。

  • resourceからresourceを実行する部分を分離し役割を減らしている
  • ryotaraiさんのアイデアで、actionを実行するのではなく事前状態と事後状態を受けて適用するようにしてみている
  • specinfraは移植しやすいよう完全にそのまま

mrubyだけでCLIを実装した知見

mrubyはまだまだ未整備な部分が多くフロンティア感がある。普通にRubyで実装するのとはいろいろ違うところがあったので、そのへんの感想を書いておく。

Rubyにはあるがmrubyに存在しなかったもの

rubygemsが使えないというのがやっぱり大きい。そして標準ライブラリは大体ないし、本家だと言語機能になっている部分がないこともあり、やっていく必要がある。以下に私のやっていきにより生まれたものを書いておく。

mruby-hashie

ItamaeはHashie::Mashをガッツリ使っているし、社内のレシピでも使われてるのでmrbgemにしておいた。元のコードがMIT Licenseなので、ライセンス表記やauthorsとともに実装をいただいたらほとんどそのまま *1 で動いた。なお、Hashie::Mashしかない。

mruby-shellwords

標準ライブラリ。かなりコマンドをガチャガチャやるので普通に必要だった。Itamaeで必要だったメソッドはそのままのコードで動いた。

mruby-open3

標準ライブラリなんだけど、これを作るのはコピペでは全然動かないし結構大変。まず既存のmrbgemにspawnがないので、それを自分で書く必要がある。spawnは本来mruby-processとかで実装すべきだけど、spawnのオプションをいろいろサポートするのは実装するのは割と大変なので、outとerrオプションを必ずredirectするインチキspawnを内蔵している。

意外とmrubyにも存在したもの

Rubyに比べてしまうともの足りなく感じるけど、mrbgem自体は結構ある。個人的にはmruby-threadとかmruby-tempfileがあるのが助かった。

GitHub - mruby/mgem-list: A list of all GEMs for mruby to be managed by mgem

ファイル名順にソースが読み込まれる

Dir.glob("#{dir}/mrblib/**/*.rb").sortの順に読まれるので、主に継承が必要な時にファイル名の先頭に数字をつけるみたいなことをやる必要がある。

mruby-requireを使うと綺麗に書けるんだけど、実行時にファイルを探しにいくのでワンバイナリを作る用途では使えなそう。

bundle gemコマンドみたいな奴

あった。便利。

github.com

まとめ

Rubyなしで実行したいコマンド作る用途にもmrubyは便利なので、この知見を参考にmrubyを使う人が増えてmrubyのエコシステムがよりよくなってほしい。あとitamae-mrubyもよろしくね

*1:&:sym でto_procする奴がなんか動かなかった。mirbでは動くので多分サポートはされてるんだけどmrbgemの中だとなぜか…。あとでちゃんと調査する

RubyなしでItamaeレシピを実行できる「itamae-go」を作った

Goとmrubyを使ってitamae-goを作った

github.com

Pokemon Goが流行っていたので流行に乗じてItamae Goを作った。

というのは冗談で、手元の開発環境のセットアップにitamaeを使っているのだけど、まっさらな環境でitamaeを実行したい時にRubyやitamaeをどういれるかについて考えるのが面倒なので、Rubyなしで実行できるitamaeを作った。Goで実装し、mrubyでレシピを読むことによりRubyなしでの実行を実現した。

インストール方法

Releasesにバイナリを置いてあるのでこれをダウンロードする。基本的には環境セットアップ用のシェルスクリプトからこれをcurlなりwgetなりでダウンロードして使うことを想定している。

なんか動かなかったらgit cloneしてmakeすればその環境用のバイナリが作れるはず。 *1

使い方

普通にitamaeレシピを書いて、

# recipe.rb
package 'vim' do
  action :install
end

service 'sshd' do
  action [:start, :enable]
end

itamae-go localを実行する。

$ sudo ./itamae-go local recipe.rb
 INFO : Starting itamae...
 INFO : Recipe: recipe.rb
 INFO : package[vim] executed will change from 'false' to 'true'
 INFO : service[sshd] executed will change from 'false' to 'true'
 INFO : service[sshd] executed will change from 'false' to 'true'

CLIは大体本家itamaeと同じになるように作っていて、Thorじゃなくてmitchellh/cliを使ってる都合でitamae helpitamae-go --helpになってるところだけ違う。itamae-go sshはなくて、itamae-go localのみ。

実装ステータス

開発環境によく使われそうなOSXとUbuntu(Debian)と、僕が使うArch Linuxをサポートしている。

実装している機能のリストはREADMEに書いてあって、_exampleに入ってるような機能は動く。 けど、一見実装されてそうな機能をいろいろサボってたり、まだ僕もそんな使ってない状況なので、自分で直しながら使うつもりの人以外は使わない方が良さそうなステータス。

Goからmrubyを使った感想

これを実装するために一昨日あたりに初めてmrubyを触った。あとプログラムに別の言語を組み込むのも初めてだったので、その辺の感想を書いておく。

良かった点

両方の言語の良いとこ取りができる

Goの豊富な標準ライブラリや高い性能を生かしつつ、RubyのDSLやmrbgemsを活用した機能を持つことができ、両方の言語の良いところがそなわり最強に見える。

mrubyが思ったよりいろいろできる

mrubyはIOがないとかrequireがないとか、あんまり普通にプログラミングできるイメージではなかったけど、mrbgemsを追加してmrubyをビルドすれば割と目欲しい機能は揃う。やんちゃコードのミルフィーユみたいな奴も動いた。

悪かった点

Goとmrubyを行ったり来たりするコードを書くのが面倒

少なくともmitchellh/go-mrubyにあるAPIでは、Rubyで作ったデータ構造をいい感じにそのままGoの世界に持ってくる手段がない。具体的にはGoでJSONを読み込む時みたいにstructにtagを書いておくとRubyのstructがそのまま読めるみたいなのをやりたい気がするが、まあなかったのでRuby側にHashをとっておくコードを書いてkey一つごとにメソッド呼び出しをすることで取得していた。*2

あとは、そもそもRubyのコードをGoの文字列として持つしかないのでその時点で汚ないコードだし、それをどこに配置するかみたいな設計も難しくなる。

クロスコンパイルが大変

というか僕がcgo有効なGoのクロスコンパイルに慣れてなくてまだできてない*3。Goとmrubyそれぞれではクロスコンパイルができるので多分できるんだろうけど、普通のGoやmrubyのクロスコンパイルに比べるとちょっと面倒そう。

気持ち

mruby-cliでもワンバイナリにできるし、mrubyになくて困ったのは__FILE__くらい*4だったので、mrubyだけで作っても良かったんじゃないかと思ったけど、組み込み言語に関する知見は結構あったので良かった。

*1:Makefileが雑なのでgoのディレクトリ規則に従った場所にgit cloneしないと通らない

*2:流石にもう少しマシな方法はあると思う

*3:なので、Releasesに置いてある奴はマシンを2台使ってあたたかみのあるビルドをしている

*4:なかったので、itamae-goでは読んでるRubyのコードのファイル名のスタックを持つようにしてるけど、それはmrubyでもできるので実装の障壁にはならない

ErgoDox2枚持ちがKinesis Contoured Keyboardを併用した感想

Kinesis Contoured Keyboardをお借りしている

id:ursm さんがKinesis Contoured Keyboardの2週間無料貸し出しをやっていて、Kinesisはずっと気になっていたキーボードだったのでお借りしてみた。 僕は今ErgoDoxを2枚所持しているが、本当はKinesisが欲しかったけど高いからErgoDoxを買ったという背景がある。

ErgoDoxに比べたKinesisの良いところ、悪いところ

まだ2日くらいしか使ってないんだけど、ErgoDoxとKinesisはおおむねキーの配置が同じですぐ慣れたしもう大体感想が固まっているので書く。

良いところ

  • 親指で押すキーが若干ErgoDoxより内側寄りなので押しやすい
    • もともと僕がこの辺のキーボード欲しくなったのはShift, Control, Alt, Spaceあたりを全部親指で押してるからで、親指で押すキーが押しやすいのはとても良い
  • キートップの形が良い
    • 中心の列のキーは結構丸みがあり、変なカドが指にぶつかることがなく触ってて気持ちいい
  • 窪んでいるので最下段のキーが押しやすい
    • ErgoDoxは一番下の列が押しづらいので、押しやすいキーの数で言うとKinesisの方が多いような気がする
  • 繋がってるのでキーボード自体の配置が安定する
    • セパレートだとキーボードの位置がズレたのをいい感じの位置に戻すみたいな手間がある。その点Kinesisはそこそこ開いてて押しやすくキーボード自体も安定もして便利。
  • ハードウェアだけでキーのリマップが完結する
    • Massdrop使うのとか自分でファームウェアビルドするのに比べてリマップが楽

悪いところ

  • デフォルトのCherry MX Brownが固め
    • キー荷重45gはCherry MX軸だと一番軽い奴なので普通は気にならないと思うんだけど、どうしてもGateron Whiteの35gが忘れられない人はつらいと思う
      • 僕もErgoDoxは両方キースイッチ取り替えやってるし、別に自分で改造できる人は変えればいいと思う。かなり面倒なのであんまりやりたくはない
  • HやGの内側にあたる場所にキーがない
    • ErgoDoxにはある奴。ErgoDoxだと最下段は押しにくい代わりにここが結構押しやすいので、もし両方使いたい場合はここがちょっと辛くなる
  • 複雑なキーリマップができなそう
    • SandSみたいに単押ししたら何かのキー、長押ししたら別の修飾キーにするみたいな奴できるんだろうか。完璧に調べたわけじゃないけど多分できなそう。

最近の気持ち

HHKBやRealforceなどの安価なキーボードを使っているみなさん

#tqrk10 で「私がRails 5に送ったパッチとその背景」について話してきた

TokyuRuby会議10でLTしてきた

去年に引き続き、今年もTokyuRuby会議10のLTに応募したら通ったので最近railsにマージされた僕のパッチで思い入れのある3つについて喋ってきた。今年はCookpad TechConfとかRails Upgrade Casual TalksでもRailsの話をしていて、なんか結構Railsの話するの好きだなと思った。

紹介した3つのパッチ

CGI.escapeHTML for html escape

rails/rails#22722。去年Ruby 2.3リリース前あたりにruby/ruby#1164CGI.escapeHTMLを高速化したんだけど、それをRailsのerbでも使われるようにした奴。テンプレートエンジンがレンダリング時に行なう処理の中でHTMLエスケープはそこそこ重い処理なので、ここの高速化は結構インパクトがある。

f:id:k0kubun:20160529212243p:plain

slimのベンチマークをActionView上で実行するようにしたbeforeからafterにかけて緑の部分だけ高速化された。Ruby 2.3とRails 5.0にアップグレードするとこの恩恵を受けられる。

ちなみにActionView::Template::Handlers::Erubisと表記しているのは、Railsが使ってるerb実装はオリジナルのerubisにいろいろ改良が加わっていて割と別物なので、その意味で本家と区別するため。

Allow joins to be unscoped

rails/rails#18109。あまりメンテしたがるコミッターがいないdefault_scopeのバグが一つ消せたというめでたい話をした。Rails 5からは(joinする)includes, eager_load, joinsでdefault_scopeが外せるようになる。

Rails 5でもまだバグは残っていて、preloadや(joinしない)includesに対してdefault_scopeを外す術がない。 *1 Railsコミッターも使えないよねと言っている機能を使うのはメンテされないという意味でも避けた方が無難。個人的にはそのバグは直って欲しいので再びパッチを出し直した*2

Alias left_joins to left_outer_joins

rails/rails#22125left_outer_joinsは2年くらい前からマージされて欲しいなーと思っていたパッチで、最初はouter_joinsという名前だったのが途中でleft_outer_joinsになり、いやいや長すぎでしょ…と思ってleft_joinsからも使えるようにしたのが僕のパッチ。

絞り込みのためだけのLEFT OUTER JOINもincludesでやるのが正しいみたいな認識をしている人がいて、ActiveRecord::Baseインスタンスの生成コストを軽視しているなと思うのだけど、健全な書き方をする方法がなかったのである意味しょうがなかった面もある。それがRails 5ではleft_joins使ってねと言えば済む話になるのでめでたい。

ついでにincludesを使う人の気持ちがわからないという話をした。ヒートアップしすぎてincludesと言う度に声が裏返っていたような気がする。

気持ち

相変わらず出てくる食べ物がおいしいし、日頃言いたかったことを好き放題喋れて、しかも結構いろんな人から共感が得られたしかなり楽しかった。去年はLT王の投票があまり集まらなかったけど、今年は2位だったのが結構うれしい。また参加したい。

*1: ただし https://github.com/rails/rails/pull/17360 のおかげでdefault_scopeを打ち消すunscopeを記述したassociationを定義しなおせばそこではdefault_scopeが外れる。まあ面倒なのでunscopedで外したい。

*2:https://github.com/rails/rails/pull/16367 の書き直し。書き直し前よりは仕様が改善してるけど、いろいろ突っ込みどころがあるのでこれではマージできないと思う。そのうちなんとかしたい。

ErgoDoxを2枚買ってキースイッチを交換した

f:id:k0kubun:20160515144142p:plain:w480

なぜErgoDoxを2枚買ったのか

直近で使っていたキーボードは1300円で買える奴で、金に余裕ができたらいい奴を買おうと思っていた。HHKBとRealforceは以前所持していたことがある*1ので、Kinesisとかを検討していたけど47kするしもう少し安いErgoDoxから試すことにした。会社まで持ち歩くのは面倒なので2枚買った。

買ったErgoDoxたち

値段は実際にクレジットカードで支払った額 (送料込み)

届くまでにかかった時間は両方2週間くらい。ErgoDox EZ→FalbaTech→PMKの順で届いた。

キーマップ

f:id:k0kubun:20160515153558p:plain

迷走中。3箇所にマッピングされている意味不明のキーもある。もともとキーマップはいじりまくってるので、それに合わせて作ってる。画像はMac用の奴で、Linux用の奴はRGui(Cmdキー)をAltに変えてる。レイヤー2にはTeensyキーと矢印キーしかないので省略した。

今はMassdropで設定してるけどGUIポチポチするの面倒なのでそのうちk0kubun/qmk_firmwareに移す。

キースイッチの変更

もともと使っていたキーボードのキー荷重が62±15gだったので、キースイッチには45gのGateron RedとGreetech Redを選んでいた。ただ、届いてみると元のキーボードよりずっと疲れた*2ので、35gのGateron Clear (ErgoDox EZのサイトだとWhite) を買ってそれに変えることにした。

で、まあ実際には半田づけをやり直すのが面倒だったので、Feelとか含め完全に同じタイプのスイッチであることを利用してバネ(とついでに軸)だけ交換することにした。

f:id:k0kubun:20160515160321p:plain:w200f:id:k0kubun:20160515160337p:plain:w200
f:id:k0kubun:20160515160340p:plain:w200f:id:k0kubun:20160515160343p:plain:w200

150個注文して150個分同じ作業をやったら6時間かかった。スイッチの分解コストがかなり高いので半田づけした方が速いかもしれない。

良い/悪いと思ったこと

ErgoDox EZ

  • 良いこと
    • Tilt/Tentキットといい感じのリストレストが最初からついてるのがかなり便利
    • キーキャップが位置によって形が違い、若干Kinesisみたいな感じになっている
    • 最初からGateron Whiteが選べる
    • 自己都合での返品による返金に対応している
  • 悪いこと
    • カバーが開けにくくカスタマイズはやりにくそう
    • 滑りにくい & ガタガタしないようにすると傾け方の選択の幅があまりない
    • FとJキーの突起が普通のキーボードよりちょっと気になる *3

FalbaTech + pimpmykeyboard: DSA PBT/ABS BLANK KEYCAP SETS

  • 良いこと
    • 外装の自由度が高い
    • 分解しやすく作りもシンプルなので壊れた時対応しやすいしカスタマイズも楽そう
    • FとJキーの突起がなく鬱陶しくない
  • 悪いこと
    • 傾けるのを自力でがんばらないといけないし、傾けると滑る
    • キーキャップの形が全部同じで、完全な平面キーボードになる
    • Gateron Whiteを採用できない

共通

  • 良いこと
    • キーボードレベルでリマップできるのでXを立ち上げる前から好きなキーマップが使える
    • セパレートなので自分が楽な姿勢を選びやすい
  • 悪いこと
    • 右端のキーの数が少ないので、JISとかUSとか関係なくいままで使っていた通りの配置を再現するのは無理

気持ち

最適なキーマップと一番楽な傾きを見つけて最高の開発環境を得たい

*1:持っていたのはJIS配列でUS配列に移行する時に売ったんだけど、当時も金がなくUS配列版を買い直せなかった

*2:これは多分普通のキーボードが押し始めにしか力がかからずそれ以降はスッと入るのに対し、Gateron/Greetechの赤軸はFeelがLinearでずっと同じ力がかかり続けるという違いがあるからだと思う。Gateron ClearもLinear。そのうちTactileとかClickyなスイッチも試したい。

*3:行ごとに形が違うのでいらないキーと交換できない

Coffee, jQueryで書いていたElectronアプリをES6, React, Reduxで書き直した

ElectronベースのTwitterクライアント: Nocturn

ElectronでYoruFukurou風のTwitterクライアントを作った - k0kubun's blog の時にCoffeeScriptとjQueryで作っていたNocturnというTwitterクライアントがあり、これをES6, React, Reduxを使って書き直した。この記事ではその時に得た知見、感じた事を書いておく。

Nocturn

移行したスタックと移行時に感じたこと

あらかじめお断りしておくと、僕は普段はRubyでサーバサイドの実装や運用をやっている人であり、JavaScriptに関してはほぼ素人の意見なので、以下はReactとかRedux興味あるけどまだ触ったことないですみたいな人向けの内容になると思う。

CoffeeScript → ES6 移行

参考: 春からはじめるモダンJavaScript / ES2015 - Qiita

CoffeeScriptはオワコンか?

Sprocketsではv4からES6のサポートが入るんだけど、リリースされているSprockets 4はまだbeta2なので、Railsアプリ書いてる人からするとむしろES6がまだ始まってない。 けどCoffeeScriptの開発はもうあまり活発ではないし、普及すればコンパイルしなくても使えるようになるES6の方が未来が明るい感じがするので移行した。

decaf

decafでcoffeeのコードをES.next に書き換える - Qiita
自動変換が結構ちゃんと動く。よーし書き直すぞと思っても手で書き直してくのは大変で、今までCoffeeScriptで書いてきた資産が変換するだけで動いてくれるのはすごく助かる。これのおかげで移行は楽だった。

babel

babelとにかく最高で、めちゃくちゃ拡張性が高くできていて、プラグインを追加するだけでJavaScriptの文法を増やすことができる。例えば僕は引数のリストにケツカンマを書かないと気が済まない人間なんだけど、これはbabel-plugin-syntax-trailing-function-commasを入れると使える。便利。

言語自体に関しては、CoffeeScriptでインデントをミスって壊れるみたいなストレスを感じなくて済むあたりは嬉しいけど、21世紀にもなってセミコロン書くのが普通な言語だし、まあどっちもどっちかなという印象。

jQuery → React 移行

比較対象がちょっとおかしい気がするけど、ここではビューのレンダリングをjQueryで生DOM操作してやるのかReact使ってVirtual DOMでやるのかみたいな意図。React、そのうち流行が過ぎて使われなくなるでしょとか思って放置してたけど意外とそうでもない雰囲気なので触ってみた。

Reactのメリット

  • Componentを操作するインターフェースが限られるので、
    • あるビューがどこから操作されるのか予測しやすい
    • 結合が疎になり必然的に再利用性が高いコードが生まれる

Reactのデメリット

  • shouldComponentUpdate*1の保守コストが高い
    • ちゃんと書かないとパフォーマンスのボトルネックになりうる
    • ミスると、更新が必要でも描画されないなどの見つけにくいバグの原因になる

というのが一番印象が強かった。Reactで書くと保守性が高くなるみたいな空気を感じるけど、僕にはshouldComponentUpdateを保守するコストがそれを相殺するように感じた。

Reduxの導入

Fluxフレームワークには、客観的に見て一番普及していて信頼性の高いReduxを採用した。 なんか@mizchiがReduxめっちゃdisってるんだけど、Redux触るまではこのまとめで言われてることがほとんど理解できなかったので*2、とりあえず彼が言ってることを理解したいというのもあって触っていた。

Reduxの良いところ

  • Fluxを知らない初心者がとっつきやすい
    • 実装の指針がそこそこあるのでSPAの設計に不慣れな初心者にはありがたい
    • http://redux.js.org のドキュメントが親切だし、情報源も多い
  • react-reduxが良い
    • Reactの描画に関して自動でいい感じのチューニングをしてくれる
    • バケツリレーが不要になる

Reduxに感じた不満

  • ファイルの数が多くなるので疲れる
    • ComponentのロジックをContainerに剥がしてくのやると結構大変
    • 分割されたActionやReducerのファイルを追加する地味な作業が頻繁に発生する
  • storeのstateに基づいてdispatchを行うContainerを書くのが大変
    • mergePropsを書く必要があるんだけど、これ書くの辛い
  • react-reduxを使っていると描画以外の部分が重くなることがある(後述)

いろいろ不満はあるけど、Reduxを導入したことで、Fluxやってない時に僕が適当に考えた設計よりはどこで何が行なわれてるかわかりやすくなったと思う。

ReactとReduxのパフォーマンスの問題

ReactとReduxで書き直したらすごい遅くなった

フルスクラッチ後、キー入力によってツイートのフォーカスを移動したりすると画面がめちゃくちゃ固まるようになった。あと、textareaに文字を打ち込むだけでもかなり重くなった。

HipChatも以前Reactで書き直されたんだけど、Reactで書き直された現在のバージョンはタブの切り替えがめちゃくちゃ重くて、そのHipChatと同じくらい重くなった。

量が多いComponentでeventをsubscribeしてはいけない

いろいろプロファイリングをやっていたんだけどなかなか原因がわからなくて、以下のツイートをした時点ではまだ勘違いしていた。

実際にはdispatchより2段ネストしたところのsetStateにかかる時間が問題になっていた。TwitterクライアントだとツイートComponentの数はかなり多くなるわけだけど、そのツイートのComponent全てがReduxのstoreのstateの変化をsubscribeしていて、ツイートの数だけsetStateされるのが重かった。

これはreact-reduxのconnectというAPIを使ってContainerを作ると自動的に行なわれる処理なので、connectをやめないといけない。数が多いComponentに接続するContainerはこういうコードを書いてconnectなしで作るようにして回避した。

shouldComponentUpdate書くの辛い

connectshouldComponentUpdateだけでなくrenderの中でもいろいろ最適化をやってくれるので、connectをやめるなら自分でシビアにshouldComponentUpdateを書かないと描画が遅くなってしまう。

React化したHipChatはチャットにいるユーザーのオンライン状況が更新されなくなったりする問題もあって、多分どこかのshouldComponentUpdateがミスってて更新されてないんだろうなという感じがする。タブ切り替えの重さも含め、Reactのこの辛さがHipChatの品質を下げているような気がしているんだけど、最近のバージョンからユーザーのアイコンが表示されるようになったりとか機能が充実してきたのも確かで、Reactによって開発しやすくなったことによってこれが達成された可能性もあるし、一概に否定はできないなと思った。

結論

ReactやReduxは大規模なアプリケーションで開発速度を維持するのには一定の効果がありそうだけど、パフォーマンスが要求されるアプリケーションでの導入には慎重になった方が良い。

*1:そのComponentを再描画する必要があるかどうかの判定

*2:ちなみに今見ても30%くらいしかわからない