Lightning フレームワークのBoxcarred Action
Lightning フレームワークを使ったコンポーネントの開発では、サーバサイドApexの呼び出しに、$A.enqueueAction()
というコールを利用します。
名前が示す通り、この呼び出しにおいては実行時にすぐにサーバ側へリクエストを投げるわけではなく、いったんキューにリクエストを格納し、ページ全体で一定時間内に要求のあったすべてのリクエストを1つのHTTPリクエストにまとめてサーバに投げる、といった挙動となります。
この仕組みは「Boxcarred Action」と呼ばれています(公式にそう呼ばれているのかどうか知りませんが、リンク先のブログを書いている人は中の人のようです)。これ(=複数リクエストを1つのリクエストにまとめる)により、HTTPリクエストのオーバーヘッドやブラウザの同時リクエスト制限を緩和・回避できるため、特に帯域の狭いモバイル環境において威力を発揮する、と言われています。
ちなみに、HTTPリクエストを1つにまとめてモバイル環境でのパフォーマンスをアップする、という話自体は特にめずらしいものではなく、有名なところではNetflixが同様のアーキテクチャを自社のAPIで選択している、という記事もあります。
- Redesigning the Netflix API
- [Embracing the Differences : Inside the Netflix API Redesign] (http://techblog.netflix.com/2012/07/embracing-differences-inside-netflix.html)
- Netflix: APIの改善と継続的デリバリー
ただ、Lightningの場合はNetflixのようにクライアントデバイスに特化するのではなくむしろ汎用的な形を目指すアプローチなので、実際にそのまま当てはめて比べるわけにはいかないとは思います。
いずれにせよ、この仕組みはLightningフレームワークの重要な売りの1つ、ということになっています。コンポーネントがそれぞれ直接APIで接続するのではなく、フレームワークが接続の代行をする(ことを強制する)ことで、すべての開発者が最適なパフォーマンスの恩恵に授かれるというわけです。よかったですね。
Boxcarred Action の有効性
では、実際にこれらはどの程度効果があるのでしょうか。検証してみましょう。
以下のようなコンポーネントを用意しました。こちらのコンポーネントは、ボタンが押されると同時にサーバサイドへ5つ並列でリクエストを要求します。
それぞれのリクエストのパラメータには、スリープ時間を設定しており、指定した時間の経過後にレスポンスが返ってくるようになっています。各リクエストはそれぞれ1秒、2秒、3秒、4秒、5秒のスリープ時間を設定しています。それぞれのリクエストがレスポンスを返した時と、最終的に5つのリクエストがすべてレスポンスを返してきた時点でタイムスタンプを取り、時間を計測しています。
({
startPerformanceTest : function(cmp, event, helper) {
var returnedCount = 0;
var parallelCount = 5;
var startTime = Date.now();
for (var i=1; i<=parallelCount; i++) {
(function(i) {
var action = cmp.get("c.requestWithSleep");
action.setParams({
requestId: ""+i,
sleepInMsec: 1000*i
});
action.setCallback(cmp, function(res) {
console.log('callbacked: ' + i);
var endTime = Date.now();
var elapsedTime = endTime - startTime;
console.log('Elapsed (req='+i+'): '+elapsedTime);
returnedCount++;
if (returnedCount === parallelCount) {
console.log('Total elapsed : '+elapsedTime);
cmp.set("v.elapsed", elapsedTime);
}
});
$A.enqueueAction(action);
console.log('requested: ' + i);
})(i);
}
cmp.set("v.elapsed", 0);
}
})
<aura:component
controller="BoxcarPerfTestController"
implements="flexipage:availableForAllPageTypes"
>
<aura:attribute name="elapsed" type="integer" />
<ui:button press="{!c.startPerformanceTest}" label="Start Performance Test" />
<aura:renderIf isTrue="{! v.elapsed > 0 }">
<div>Total elappsed time: <span>{!v.elapsed}</span> msec</div>
</aura:renderIf>
</aura:component>
以下はサーバ側(Apex)のコードです。Apexでは任意時間スリープさせる機能はないので、HTTPコールアウトで代用します。Herokuのコードはここでは書きませんが、ただ単にパラメータで渡した時間分だけ待ち、レスポンスを返すものです。
public class BoxcarPerfTestController {
@RemoteAction
@AuraEnabled
public static Boolean requestWithSleep(String requestId, Integer sleepInMsec) {
String echoServiceUrl = 'https://sleeping-test.herokuapp.com/echo';
echoServiceUrl += '?requestId=' + requestId + '&sleepInMsec='+sleepInMsec;
Http h = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint(echoServiceUrl);
req.setMethod('GET');
HttpResponse res = h.send(req);
return res.getStatusCode() == 200;
}
}
実際にこれを実行してみました。以下はその結果(コンソールメッセージ)です
requested: 1
requested: 2
requested: 3
requested: 4
requested: 5
callbacked: 1
Elapsed (req=1): 16847
callbacked: 2
Elapsed (req=2): 16848
callbacked: 3
Elapsed (req=3): 16848
callbacked: 4
Elapsed (req=4): 16849
callbacked: 5
Elapsed (req=5): 16849
Total elapsed : 16849
それぞれのリクエストは1秒-5秒の処理であるはずなのに、なんとすべて17秒近くかかってしまいました。これは一体何が起きているのでしょうか。
レスポンスの時間がすべてのリクエストで同じタイミングになる、というのは、なんとなく理解できます。1つのHTTPリクエストを共有しているのですから、特別な工夫をしない限りは実際のHTTPレスポンスがブラウザに返ってきた時にすべてのコールバックが呼び出されるだろうからです。しかし、その場合でも、リクエスト中の最大処理時間である5秒程度になることを期待していたのですが、まったくひどい結果になってしまいました。
もしかして、enqueueAction()
でキューされたすべてのリクエストは逐次処理されているのではないでしょうか。もしそうだとすると、確かに17秒という時間も1+2+3+4+5=15秒であることを考えると納得がいきます。
しかしこれでは、さすがにモバイル環境への最適化をねらっていたとしても、1つ1つHTTPリクエストを送ったほうがはるかに効率が良さそうなのは、Lightningの初心者でも分かりそうなものです。というか、とてもこれはプロダクションでは使えないのではないでしょうか。
VisualforceでのRemoting
ちなみにBoxcarred ActionはLightningだけのものというわけではなく、VisualforceのJavaScript Remotingにも同様のことを行うbuffer
というオプションがあります。こちらでも同じように逐次処理になってしまうのでしょうか。
以下の様なVisualforce Pageを作成し、同様に計測を行いました。
<apex:page showHeader="false" docType="html-5.0"
applyBodyTag="false"
applyHtmlTag="false"
controller="BoxcarPerfTestController"
>
<html>
<head>
<script>
function startPerformanceTest(buffered) {
var returnedCount = 0;
var parallelCount = 5;
var startTime = Date.now();
for (var i=1; i<=parallelCount; i++) {
(function(i) {
BoxcarPerfTestController.requestWithSleep(""+i, 1000*i, function(result, event) {
var endTime = Date.now();
var elapsedTime = endTime - startTime;
console.log('Elapsed (req='+i+'): '+elapsedTime);
returnedCount++;
if (returnedCount === parallelCount) {
console.log('Total elapsed: ', elapsedTime);
}
}, { buffer: buffered });
console.log('requested: ' + i);
})(i);
}
}
</script>
</head>
<body>
<button onClick="startPerformanceTest(true)">Start Performance Test(Buffered)</button>
<button onClick="startPerformanceTest(false)">Start Performance Test(Not Buffered)</button>
</body>
</html>
</apex:page>
上記コードではRemotingの呼び出しに渡すオプションのbufferの値(true/false)を切り替えられるようになっています。まずはbufferを有効にした場合の結果(コンソールメッセージ)を以下に示します。
requested: 1
requested: 2
requested: 3
requested: 4
requested: 5
callbacked: 1
Elapsed (req=1): 17844
callbacked: 2
Elapsed (req=2): 17845
callbacked: 3
Elapsed (req=3): 17845
callbacked: 4
Elapsed (req=4): 17845
callbacked: 5
Elapsed (req=5): 17845
Total elapsed: 17845
Lightningと同じような結果になってしまいました。続いてbufferを無効化した場合です。
requested: 1
requested: 2
requested: 3
requested: 4
requested: 5
callbacked: 1
Elapsed (req=1): 1895
callbacked: 2
Elapsed (req=2): 2919
callbacked: 3
Elapsed (req=3): 4045
callbacked: 4
Elapsed (req=4): 4967
callbacked: 5
Elapsed (req=5): 5993
Total elapsed: 5993
それぞれのリクエストは実際に設定した処理時間とほぼ変わらない経過時間でレスポンスが返ってきました。トータルでも6秒程度なので、bufferを指定した場合と比べて圧倒的です。
以上から分かることは、どうもRemotingのbufferにしろLightningのBoxcarred Actionにしろ、フレームワーク提供者が唱えているほど素晴らしいものではなさそうだ、ということです。しかも、おせっかいなことに Remoting ではデフォルトでbufferは有効化されているので、ちゃんと並列で実行しようと思ったらbufferオプションを常にfalseに設定してやらないといけません。
ではLightningの方はどうでしょうか。どうやらこのブログの記事をみると、enqueueAction()
に渡すactionに対してsetExclusive()
というメソッドを記述してあげることで、該当のアクションに占有のHTTPリクエストを割り当てできるように設計されているようです。
しかしながら、本記事の執筆現在では、このコードを追加してもリクエストの実行にはまったく影響がないような結果となっています(常にBoxcarred Actionとなる)。
考察
そもそもの話なのですが、たとえ上記のような問題がなかったとしても、一体どのような場合にBoxcarred Actionに効果があるのでしょうか?
ここで、Boxcarred Actionが威力を発揮するといわれているモバイルデバイスでの利用を考えてみます。
Lightningでは、いくつかのLightningコンポーネントを組み合わせ、Lightningページを構成します。件のBoxcarred Actionは同時にサーバリクエストが発生するような状況でなければ有効にはならないなので、ページロード時がメインの活躍機会になるかと思います。しかしながら、スマートフォンなどのモバイルデバイスは現状画面の大きさの制約が厳しく、一度に表示するコンポーネントというのはそれほど多くはありません。せいぜい2,3といったところでしょうか。はたしてその程度のリクエストを重ねあわせることがそれほど重要かというと、疑問符がつきます。
なので、Salesforceがアナウンスしているのとは異なり、実はモバイル環境ではBoxcarred Action機能の活躍の機会はあまりないのではないか、というのが自分の意見です。
ではもっと多数のコンポーネントが一度に表示されるような状況 - たとえばダッシュボードだったりポータル画面のようなものを考えてみましょう。これは、タブレットという場合もあるでしょうが、多くはPC画面からのアクセスになるでしょう。そしてこれは近々リリースされるであろうデスクトップ版のLightningがカバーする領域になるはずです。
そしてPCの場合は、ネットワーク帯域についても十分である場合が多く、これまたBoxcarred Actionが必要かというとそうでもなさそうです。
しかも、もしBoxcarred Actionの実行が今のまま逐次実行であった場合、Lightningコンポーネントをページに追加すればするほど画面の初期表示はどんどん重くなってしまいます。これでは今と変わらないどころかさらにユーザ体験は悪化するでしょう。なんとかならないものでしょうか。
まとめ
- enqueueActionはリクエストをサーバサイドで逐次実行してしまう
- Visualforce JS Remotingの場合はbuffer:falseで並列化可能で、こちらのほうが実際かなり早い
- 公式発表を鵜呑みにするな、ということがよく分かる事案