pixiv insideは移転しました! ≫ https://inside.pixiv.blog/

JSがブラウザを固めてつらいので、新しいAPI「requestIdleCallback」を使うことにした

ピクシブ株式会社 Advent Calendar 2015」の24日目の記事です。こんにちは、今年8月からピクシブに入社した新米の@_furoshikiです。入社直後は、古くなって大変なことになっているJavaScriptのコードを、そーっと触る系エンジニアをやっていました!

私はピクシブに入る前から、オフィスの一部を借りて「東京Webパフォーマンス」という趣味丸出しなイベントを開催しています。その中で扱ったテーマのひとつである「requestIdleCallback」が、古いJavaScriptのコードにUIをブロックされ、荒んでいた私の心を救ってくれました!

今回、このrequestIdleCallbackというブラウザの新機能について、pixiv insideのブランド力を信じて、日本に多く広まるよう宣伝してみようかとおもいます!

そもそも、ブラウザのなにが問題なのか?

WindowsやAndroid、iOSなどのオペレーティングシステムは、スクリーン上に色や線などの描画(レンダリング)するためのAPIを持っています。しかしそれは、複数のスレッドから扱うことを許していません。ブラウザがレンダリングを扱うアプリケーションである以上、この制約はつねにのしかかってきます。

CPUはマルチコアがあたりまえの時代です。同時に2つ以上の処理を実行する「マルチスレッド」で動かすことで、パフォーマンスを改善していく時代です。最近は、マルチスレッドでレンダリングできるような環境も発明されています。しかし、どれだけ世の中が進んでも、ブラウザがどんな環境でも動かなくてはいけない以上、シングルスレッドでレンダリングすることが求められています。ブラウザの中では、JavaScriptをはじめさまざまな処理が動いていますが、これらは全てシングルスレッドで動かなくてはいけません。

この問題を、ブラウザは「タスクキュー」という仕組みを使って対応しています。ブラウザ内のさまざまな処理を「タスク」としてあつかい、「キュー」と呼ばれる列にならべて、順番に実行させていきます。

f:id:devpixiv:20151217010303j:plain

この仕組みには問題があります。ひとつのJavaScriptのタスクに長い時間を使っていると、ブラウザはその間、レンダリングはおろか、クリックなどの入力さえも受け付けてくれなくなります。キューに並んでいる別のタスクがまったく実行されないため、「フリーズ」という状態に陥るのです。

JavaScriptの処理は、実行時間の短いタスクへと分割して、レンダリング系のタスクがキューに並ぶ余地を作りつつ実行させていくことが求められています。最近では、コールバックをはじめとして、Promise、Generator、co、async/awaitなど。タスクの実行時間を小さく分割して、キューにならべていく手段が次々と整備されていきました。

requestIdleCallbackが解決する、タスクキューの混雑問題

HTML5やJavaScriptフレームワークなど、JavaScriptにやらせることが増え、そこにまた別の課題が生じます。ブラウザのタスクキューが混雑してしまうのです。せっかくレンダリング系のタスクがキューに並ぶ隙を作ったのに、列が長すぎて順番がなかなかまわってきません!

f:id:devpixiv:20151223021046p:plain

このため、「レンダリング系のタスクにスレッドをゆずり、JavaScriptのタスクは優先度を下げて実行したい!」というニーズが生じました。その解決方法がrequestIdleCallbackです。requestIdleCallbackは、キューの中が空でかつ、スレッドが何のタスクも処理していないタイミングで、タスクを実行させるというブラウザの新機能です。

f:id:devpixiv:20151224013042j:plain

使い方はこんな感じで、requestIdleCallbackというメソッドに、タスクをコールバックとして渡します。

var hd = requestIdleCallback(function(){
  // タスクキューが空になってから実行されるタスク
});

キューがつねにいっぱいだと「諦める」という選択もできます。タイムアウトの時間を指定できるのです。こちらはまだ実装が怪しいので、2015年12月の今の段階では、使わない方がいいかもしれません :)

var hd = requestIdleCallback(function(){
  // タスクキューが空になってから実行されるタスク
},{ timeout: 10000 });

async/awaitのような非同期プログラミングにみられる、タスクの「キャンセル」も実装されています。

cancelIdleCallback( hd );

とてもシンプルですね!

現場でつかった際の効果

requestIdleCallbackは、私の関わっているサービスでも、ところどころに活用しています。その際に検証したデータをお見せしましょう。

ひとつめのデータは、requestIdleCallbackをいっさい使わず、処理を1つのタスクに固めて実行させたものです。黄色い部分がJavaScriptの実行になります。メソッド呼び出しによってがんがんネストされ、コールスタックが下に向かって膨らんでいます。いつになっても、レンダリング系のタスクにスレッドをゆずってくれません。

f:id:devpixiv:20151216221516p:plain

これを非同期に実行できるタスクへと分割して、requestIdleCallbackで実行するとどうなるでしょう?緑の部分が、レイアウトなどのレンダリング系のタスクです。JavaScriptの実行処理である黄色い部分がバラバラにちらばり、ブラウザのレンダリングを阻害しないようにうまく分散されています。

f:id:devpixiv:20151216222115p:plain

よく見ると、せっかく空いた時間に処理を分散させているのに、レンダリング処理を阻害している重いJavaScript処理が2つほど走っています。調査したところ、jQuery UIの初期化処理が原因でした。

jQuery UIの初期化は、初期化用のメソッドがよびだされてからreturnするまでのあいだに、すべての処理を完了させてしまうという、旧時代的なやりかたを採用しています。処理がまったく分散されていないので、パフォーマンスで完全に足を引っ張ってしまっています。

私が作ったUIでは、ATF(Above The Fold)パフォーマンスの改善に活用しています。一番最初に表示される領域に必要なJavaScriptのタスクを最優先で実行させ、見えていない領域はrequestIdleCallbackを使って遅延実行させています。目に見えるほどの効果があったので、この使い方はきにいっています。

f:id:devpixiv:20151223023146p:plain

requestIdleCallbackは、FirefoxやEdgeで実装されることが決定しており、また現在最新のChrome 47ではすでにサポートが開始されるなど、そろそろ活用が進みそうな気配です。仕様の標準化を行っているW3Cでは、初夏ぐらいに草案を作り始めたということもあり、今も活発に議論がされています。私も標準化サイドに問題提起した機能だったりするので、普及して人々を幸せにしてくれることを心から願っています!

ちなみに、現場で使いたいという人には、このあたりのPolyfillが参考になるかもしれません。ご一読を!

もっと知りたい?

TechBoosterの合同誌「The Web Explorer」をご覧ください!コミックマーケット89にて頒布予定ですが、私が今開発に関わっているサービス「BOOTH」でも先行予約を受け付けています。買ってくれたら、この本の執筆者の打ち上げ会場が、ちょっとだけ豪華になります。

techbooster.booth.pm

余談ですが、TechBoosterはBOOTHの「倉庫に後から入荷」機能を使って、同人活動で問題になりがちな在庫リスクを下げています!同人誌は印刷費がびっくりするほど高いのですが、事前に予約したり、お金を払ってもらうことで、受注生産的なことをやっています。この仕組みを上手くつかえば、原価割れが怖くてなかなか発表できなかった作家さんにもチャンスがありそうです!

さて、ピクシブは創作をする人たちのリスクを下げることはできても、Webの技術は進化が速く、開発のリスクはいつになっても下げることができません!JavaScriptを書けるエンジニアが不足しているので、いつになってもタスクキューが空にならないのです。これでは、いつになってもrequestIdleCallbackにコールバックされる日がやってきません!

ピクシブでは「オレがお前らJavaScriptエンジニアのrequestIdleCallbackをコールバックさせちゃうぜ!」というエンジニアを募集しています。

recruit.pixiv.net

最終日である明日は、ピクシブのリードエンジニアである@edvakfの記事です!お楽しみに!