LoginSignup
282
283

More than 5 years have passed since last update.

JavaScriptの1msは4ms

Last updated at Posted at 2016-10-20

はじめに

JavaScriptのタイマー処理(setTimeout(), setInterval())は1000分の1秒の精度(ms)で遅延時間を指定することができます。

setTimeout(function() {
    // 1ms後に実行してほしい
    alert('hello!');
}, 1);

見ての通り、上記のコードは第一引数に渡した関数が1ms後に実行されることを期待して書かれていますが、実際にこれを実行すると4ms以上の遅延が発生する可能性があります。

これは、JavaScriptエンジンの実装によって、4ms以下の設定値は4msに矯正するという処理が行われているためです。

※追記)厳密には上記処理のみを実行した場合は4msへの矯正は行われないようです。4msへの矯正が行われる条件として当記事最下部に追記を行っておりますので併せてご参照下さい。

なぜ4msなのか

実はこの4msという数字、HTML5の仕様で明確に定められています。

HTML Standard - 8.5 Timers

2010年以降にリリースされたブラウザはおおむねこの仕様に則っているので、現在広く利用されているモダンブラウザ(Chrome, FireFox, 等)のタイマー最小解像度は4msに設定されています。

実際のところ、それ以前のブラウザでは搭載しているJavaScriptエンジンの実装によってタイマーの最小解像度は異なりました。
FireFox3, IE8では16ms前後、Chrome4では4ms前後、Opera10では2.5ms前後といった具合です。

※過去、Chromeはタイマーの最小解像度を1msに設定していましたが、消費電力の激しさから4msに修正したという歴史があったりもします。

実際に計測してみる

ということで実測値が4msになるかの検証です。
次のコードを各ブラウザごとに5回ずつ実行させて検証を行いました。

function checkResolution(count)
{
    let logs = [];
    let index = 0;

    const split = function() {
        if (index < count) {
            index++;
            setTimeout(function() {
                logs.push(Date.now());
                split();
            }, 1);
        } else {
            alert(calcAverage());
        }
    };

    const calcAverage = function() {
        let sum = 0;
        logs.reduce(function(prev, current) {
            sum += current - prev;
            return current;
        });

        return sum / logs.length;
    };

    split();
}

document.addEventListener('DOMContentLoaded', function() {
    checkResolution(1000);
}, false);

ロジックとしてはsetTimeout()にて実行日時のタイムスタンプを記録する関数を1ms毎に1000回呼び出し、記録されたタイムスタンプ間の間隔(ms)の平均値を算出するようになっています。
パフォーマンスに影響を与えないよう、ドキュメントのロード後に実行するようにしています。

検証したブラウザはGoogle Chrome, FireFox, Opera, IE, Edgeの5つで、各ブラウザのバージョンは次のとおりです。

ブラウザ バージョン
Google Chrome 54.0.2840.59 m
FireFox 49.0.1
Opera 40.0.2308.90
IE 11.321.14393.0
Edge 38.14393.0.0

実行環境のスペックはOS:Windows10 64bit, CPU:i7@3.4GHz, 実装メモリ:32GBとなります。

検証結果

下記表の単位はすべてmsとなります。

Google Chrome

1回目 2回目 3回目 4回目 5回目
4.664 4.637 4.706 4.695 4.673

FireFox

1回目 2回目 3回目 4回目 5回目
4.375 4.343 4.467 4.403 4.474

Opera

1回目 2回目 3回目 4回目 5回目
4.724 4.655 4.67 4.645 4.647

IE

1回目 2回目 3回目 4回目 5回目
3.59 3.937 3.93 3.753 3.559

Edge

1回目 2回目 3回目 4回目 5回目
1.882 1.911 1.976 1.927 1.891

Google Chrome, FireFox, OperaはHTML5の仕様どおり、4ms~5msあたりの値を得ることが出来ました。いい子ですね。
...が、IEで嫌な予感を感じ始めEdgeで考えるのをやめました。

この結果を見て、「Edge早すぎィ」なのか「お作法に則ってないやり直し」となるのかは意見が割れそうですね。

番外編

前述の計測コードをNodeJSでも実行してみました。バージョンはv6.9.0です。

1回目 2回目 3回目 4回目 5回目
1.55 1.602 1.531 1.582 1.579

フロントサイドとは異なり、サーバーサイドJSでは4ms制限は特に設けられていないようですね。にしても早ひ。
※ちなみに関数呼び出し間隔を0msに設定し試してもみましたが、上記とほぼ同様の結果でした。

おわりに

Webアプリケーションを作成する上で厳密に1msにこだわらなければならない場面はなかなか無いかもしれません。
しかし万が一遭遇してしまったとしてもJavaScriptのタイマー処理を信頼しすぎる設計はかなり危険です。
たとえば4ms以上の設定値であったとしても1~2msのズレは往々にして起こります。
それはJavaScriptのタイマー精度に起因している場合もあれば、マシンへの過負荷によって生じている可能性もあります。

JavaScriptのタイマー処理は用法用量を守って正しく使いたいものですね!(戒め)


※追記(2016-10-21)

4msの矯正が行われる条件は、あくまでもnesting level5より大きい場合のみである、とのご指摘を頂きましたので再調査しました。
上記の検証用関数checkResolution()に渡す引数をnesting level以下の値(4, 等)で試した所、確かに4msに矯正されるような挙動は見られなくなりました。

参考までにGoogle Chromeにて再テストを行った結果を載せておきます。

※またDate.now()よりもperformance.now()のほうが精度的に優れるとのご指摘も頂きましたので、関数を修正した上で計測を行っております。

- 測定値(ms)
1回目 1.2662499999999994
2回目 1.4837499999999864
3回目 1.28125
4回目 1.4624999999999915
5回目 2.094999999999999

試行回数が少ないため値のバラ付きは見られますが、nesting level5以下の状態においては設定値付近(1ms~2ms)の値を得られることが分かりました。

誤解を招いてしまい申し訳ありません。正しくはnesting level5より大きい場合、最低解像度が4msに矯正される」ということになります。

282
283
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
282
283