« どこでも「do a barrel roll」するためのグリモンとブックマークレット | メイン | Androidで動かせるWebSocket Server「WSBroadcaster for Android」 »

モダンブラウザのタイマー処理の制限をWebSocketを使って突破する(WebWorkersについて追記あり)

最近のブラウザは、パフォーマンス改善のために、バックグラウンドタブのsetTimemout/setIntervalの実行遅延の下限を1000msに制限している。
例えば、フォアグランドとバックグラウンドで、以下のようなコードの挙動を比べてみるとよくわかる。
(
    function() {
        var ms = 100;
        var old = new Date().getTime();
        var f = function(){
            var now = new Date().getTime();
            console.log((now-old)+"ms");
            old = now;
            setTimeout(f,ms);
        }
        setTimeout(f,ms);
    }
)();
(
    function() {
        var ms = 100;
        var old = new Date().getTime();
        var f = function(){
            var now = new Date().getTime();
            console.log((now-old)+"ms");
            old = now;
        }
        setInterval(f,ms);
    }
)();
アニメーション処理であれば、今後はrequestAnimationFrameを使ったりCSS3のTransitionsやAnimationsを使うというのが正道なんだろう。
アニメーション以外の処理は、下限を1000msに制限されても正常に動作させるようにコードを修正するのが筋なんだろうと思う。
しかし、「どうしてもsetTimout/setIntervalでないといけない」「1000ms未満の間隔で実行しなければいけない」等の要望もあるかもしれない。
そこで、イベントドリブンな処理は制限されないことを利用して、setTimemout/setIntervalとほぼ同等な処理を間隔1000ms未満で実行できるコードを書いてみた。
まだ、実験コードレベルなので注意。
遅延処理をサーバ側で行い、実行するタイミングでクライアントに通知することで実現している。
通信にはWebSocketを使っているので一部のブラウザではまだ動作しないが、この問題はブラウザのバージョンアップで早々に解決すると思う。
コードはGitHubに上げてある。
インストール方法や使い方もGitHub上のREADMEを参照。
以下、使い方のサンプル。
<!DOCTYPE html>
<html>
<head>
<title>ForcedTimer sample</title>
<script type="text/javascript" src="../src/client.js"></script>
<script type="text/javascript" src="sample.js"></script>
<link rel="stylesheet" type="text/css" href="sample.css">
</head>
<body>
<div id="timeout_area">
<input type="button" id="timeout_button" value="set/clear timeout" />
<textarea id="timeout_output"></textarea>
</div>
<div id="interval_area">
<input type="button" id="interval_button" value="set/clear interval" />
<textarea id="interval_output"></textarea>
</div>
</body>
</html>
html, body{
    margin:0;
    padding:0;
    height:100%;
}
#timeout_area{
    width:49%;
    height:99%;
    float: left;
}
#interval_area{
    width:49%;
    height:99%;
    float: right;
}
#timeout_button, #interval_button{
    display:block;
}
#timeout_output, #interval_output{
    width:80%;
    height:80%;
    font-size: 12px;
    font-family: monospace;
    display:block;
}
var initialize = function() {

    var timeoutButton  = document.getElementById("timeout_button");
    var intervalButton = document.getElementById("interval_button");
    var timeoutOutput    = document.getElementById("timeout_output");
    var intervalOutput   = document.getElementById("interval_output");

    var ms = 100;
    var timerIds = {};

    var getDate = function() {
        var dtm = new Date();
        var result =
            ("0000" + (dtm.getFullYear()     + 0)).slice(-4) + "/" +
            (  "00" + (dtm.getMonth()        + 1)).slice(-2) + "/" +
            (  "00" + (dtm.getDate()         + 0)).slice(-2) + " " +
            (  "00" + (dtm.getHours()        + 0)).slice(-2) + ":" +
            (  "00" + (dtm.getMinutes()      + 0)).slice(-2) + ":" +
            (  "00" + (dtm.getSeconds()      + 0)).slice(-2) + "." +
            ( "000" + (dtm.getMilliseconds() + 0)).slice(-3) + ""  ;
        return result;
    };

    var timeout = function() {
        timeoutOutput.value = getDate() + "\n" + timeoutOutput.value;
        timerIds["timeout"] = ForcedTimer.setTimeout(timeout, ms);
    };

    var interval = function() {
        intervalOutput.value = getDate() + "\n" + intervalOutput.value;
    };

    timeoutButton.addEventListener(
        "click",
        function() {
            if ("timeout" in timerIds) {
                var timerId = timerIds["timeout"];
                ForcedTimer.clearTimeout(timerId);
                delete timerIds["timeout"];
            } else {
                timerIds["timeout"] = ForcedTimer.setTimeout(timeout, ms);
            }
        },
        false
    );

    intervalButton.addEventListener(
        "click",
        function() {
            if ("interval" in timerIds) {
                var timerId = timerIds["interval"];
                ForcedTimer.clearInterval(timerId);
                delete timerIds["interval"];
            } else {
                timerIds["interval"] = ForcedTimer.setInterval(interval, ms);
            }
        },
        false
    );
};

window.addEventListener("load", initialize, false);
2011/11/10 追記
Twitter上でツッコミがありました。
気になったので試したWebWorkersの中のtimerはバックグラウンドタブでも精度荒くならない at Firefox7
バックグラウンドタブでも通常精度でタイマー動かしたいときにはWebWorkersで setInterval(function(){ postMessage("10msたったぞ") }, 10) とかやればいい
「なんですとぉ!」ってことで試してみた。
指摘された通りバックグラウンドタブでも1000ms未満で動いた。
malaさんありがとうございます。
せっかくWebSocketで通信インターフェイスを作ったので、WebWorkers版も同様の作りにして動作するようにしました。
WebWorkers版もGitHubにあげているので興味のある方はどうぞ。

コメントを投稿

(いままで、ここでコメントしたことがないときは、コメントを表示する前にこのブログのオーナーの承認が必要になることがあります。承認されるまではコメントは表示されません。そのときはしばらく待ってください。)

Google

タグ クラウド