はじめに
JavaScriptのタイマー処理(setTimeout()
, setInterval()
)は1000分の1秒の精度(ms)で遅延時間を指定することができます。
setTimeout(function() {
// 1ms後に実行してほしい
alert('hello!');
}, 1);
見ての通り、上記のコードは第一引数に渡した関数が1ms
後に実行されることを期待して書かれていますが、実際にこれを実行すると4ms以上の遅延が発生する可能性があります。
これは、JavaScriptエンジンの実装によって、4ms
以下の設定値は4ms
に矯正するという処理が行われているためです。
※追記)厳密には上記処理のみを実行した場合は4ms
への矯正は行われないようです。4ms
への矯正が行われる条件として当記事最下部に追記を行っておりますので併せてご参照下さい。
なぜ4msなのか
実はこの4ms
という数字、HTML5の仕様で明確に定められています。
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 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
に矯正される」**ということになります。