はじめに
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:[email protected], 実装メモリ: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に矯正される」**ということになります。