JavaScriptの1msは4ms

# はじめに

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

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

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

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


**※追記）**厳密には上記処理**のみ**を実行した場合は`4ms`への矯正は行われないようです。`4ms`への矯正が行われる条件として[当記事最下部](#%E8%BF%BD%E8%A8%982016-10-21)に追記を行っておりますので併せてご参照下さい。

# なぜ4msなのか

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

[HTML Standard - 8.5 Timers](https://html.spec.whatwg.org/multipage/webappapis.html#timers)

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

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

※過去、`Chrome`はタイマーの最小解像度を`1ms`に設定していましたが、消費電力の激しさから`4ms`に修正したという[歴史](https://src.chromium.org/viewvc/chrome?view=revision&revision=3170)があったりもします。


# 実際に計測してみる

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

```js
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 level`が`5`より大きい場合のみである、とのご指摘を頂きましたので再調査しました。
上記の検証用関数`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 level`が`5`以下の状態においては設定値付近(`1ms`~`2ms`)の値を得られることが分かりました。

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