重いループのCPU負荷を軽減し、体感的・心理的に高速化するJavaScriptライブラリ。
chillout.js は「処理時間を短くする」という物理的な高速化とは違い、CPU負荷を軽減しリソースに余裕を持たせ、重い処理でも軽く感じさせることでユーザーにとって体感的・心理的な高速化につなげる JavaScript ライブラリです。
重いWebページやゲームでは画面が読み込み中のままカクカクだったり、Webページに限らずnode.jsでバッチ処理するときでも高CPU負荷が続くとマシンごと重くなってしまいます。
特にスペックの低い端末では、CPU使用率100%の状態が続くと加熱されて冷却ファンが激しく回り、そのまま使い続けると冷却が追いつかずに熱暴走してしまう可能性もあります。
重い処理のほとんどはループ処理によって発生します。ループの中でさらにループ、その中でさらにループ…。単純に考えた場合、そうならないようループの途中で一定時間処理を休止させればいいんですが、それができません。
JavaScript には一定の時間休む sleep のような機能がないからです。そこで、sleepするにはどうするか?というと「非同期」でループ処理します。
JavaScript では、setTimeout
や process.nextTick
を使って同期処理を非同期化できます。
それと Promise
を組み合わせると sleep のように一定時間CPUを休ませる非同期処理が実現できます。
処理の高速化というと、とにかく1ミリ秒でも処理時間を短くすることが手法とされますが、Webページやアプリ、ゲームといった、人が画面を見たり操作する場合 心理的に「速い」と感じればユーザーのストレスが減り結果として高速化につながります。
chillout.js は、ループ処理が重いときにはCPUが休まるくらいの休止時間、処理が速いときには休止時間なしか、わずかな休止時間をいれ本来のループを邪魔しないようにし、結果としてカクカクするような重さを感じさせずにループを実行します。
ほとんどの場合、既存の JavaScript ループの代わりに chillout.js のループを使うことで CPU 使用率を減らすことができます。非同期ループとして扱う必要がありますが async/await
を使うと扱いやすくなります。
また、処理が重くなるとでる 「警告: 応答のないスクリプト」 といったブラウザ警告なしでJavaScriptを実行できます。
ブラウザ上、Electron、Node.js などの環境で使えます。
$ npm install chillout
jsdelivr.com または cdnjs.com で CDN が利用できます。
require
で使う場合の例:
var chillout = require('chillout');
chillout.forEach([1, 2, 3], function(value) {
console.log(value);
}).then(function() {
console.log('done');
});
// 1
// 2
// 3
// 'done'
ブラウザで実行してる場合は、chillout というオブジェクトがグローバル ( window.chillout
) に定義されます。
Promise
が動けば使えます(最新のブラウザはどれも動きます)。
(古い環境で Promise
がサポートされてない環境の場合は、es6-shim や、他の Promise
polyfill を使ってください。)
async/await
が使える環境ならより簡潔に書けます。 chillout.js のAPIはすべて Promise を返すため async/await
で扱えるようになっています。
forループと chillout.repeat
を比較します。
function heavyProcess() {
var v;
for (var i = 0; i < 5000; i++) {
for (var j = 0; j < 5000; j++) {
v = i * j;
}
}
return v;
}
var time = Date.now();
for (var i = 0; i < 1000; i++) {
heavyProcess();
}
var processingTime = Date.now() - time;
console.log(processingTime);
- 処理時間: 107510ms.
- CPU平均使用率(Nodeプロセス): 97.13%
var time = Date.now();
chillout.repeat(1000, function(i) {
heavyProcess();
}).then(function() {
var processingTime = Date.now() - time;
console.log(processingTime);
});
- 処理時間: 138432ms.
- CPU平均使用率(Nodeプロセス): 73.88%
ForStatement (for文) | chillout.repeat | |
---|---|---|
処理時間 | 107510ms. | 138432ms. |
CPU平均使用率(Nodeプロセス) | 97.13% | 73.88% |
chillout.repeat
は forループ よりも低いCPU使用率で実行されているのが確認できます。
chillout.js は、より低いCPU使用率と自然な速さでJavaScriptを実行できますが、処理速度は少し遅くなります。
JavaScriptのパフォーマンスにおいて最も重要なことの一つは、数値的な速度ではなく安定したレスポンスによってユーザーにストレスを与えずに実行することです。これはブラウザ上で実行される場合、体感的な高速化の手段として特に重要です。
(ベンチマーク: chillout v3.1.2, Windows8.1 / Intel(R) Atom(TM) CPU Z3740 1.33GHz)
npm run benchmark
でベンチマークを実行できます。
与えられた関数 callback
を、配列またはオブジェクトの各要素に対して一度ずつ実行します。
関数内で chillout.StopIteration
を返すか、エラーが発生すると、それ以降のループ処理は実行されません。
このメソッドは JavaScript の Array forEach
のように使えます。
- chillout.forEach ( obj, callback [, context ] )
- @param {array|object} obj 対象の配列またはオブジェクト。
- @param {function} callback 各要素に対して実行するコールバック関数で、3つの引数をとります。
- value: 現在処理されている配列の要素、またはオブジェクトの値。
- key: 現在処理されている配列の要素のインデックス、またはオブジェクトのキー。
- obj:
forEach
が適用されている配列またはオブジェクト。
- @param {object} [context] 任意。コールバック内で
this
として使用する値。 - @return {promise} Promiseオブジェクトを返します。
配列のループ例:
var values = ['a', 'b', 'c'];
chillout.forEach(values, function(value, key, obj) {
console.log(value);
}).then(function() {
console.log('done');
});
// 'a'
// 'b'
// 'c'
// 'done'
オブジェクトのループ例:
var values = {
a: 1,
b: 2,
c: 3
};
chillout.forEach(values, function(value, key, obj) {
console.log(key + ':' + value);
}).then(function() {
console.log('done');
});
// 'a:1'
// 'b:2'
// 'c:3'
// 'done'
async / await
を使ってループする例:
この例はファイルの内容をすべて出力し、最後に 'done' を出力します。
async function getFileContents(url) {
const response = await fetch(url);
return response.text();
}
// chillout.forEachのコールバックでasync functionを渡す
async function logFiles() {
const files = ['/file1.txt', '/file2.txt', '/file3.txt'];
await chillout.forEach(files, async url => {
const contents = await getFileContents(url);
console.log(contents);
});
console.log('done');
}
logFiles();
与えられた関数 callback
をを、引数で与えられた数だけ実行します。
関数内で chillout.StopIteration
を返すか、エラーが発生すると、それ以降のループ処理は実行されません。
このメソッドは JavaScript の for
ステートメントのように使えます。
- chillout.repeat ( count, callback [, context ] )
- @param {number|object} count 繰り返す回数またはオブジェクトで指定。
オブジェクトで指定する場合は以下のキーが有効です。- start: 開始する数。
- step: ステップ数。
- done: 終了する数。
- @param {function} callback 各ループに対して実行するコールバック関数で、1つの引数をとります。
- i: 現在の数。
- @param {object} [context] 任意。コールバック内で
this
として使用する値。 - @return {promise} Promiseオブジェクトを返します。
- @param {number|object} count 繰り返す回数またはオブジェクトで指定。
回数を指定する例:
chillout.repeat(5, function(i) {
console.log(i);
}).then(function() {
console.log('done');
});
// 0
// 1
// 2
// 3
// 4
// 'done'
オブジェクトで指定する例:
chillout.repeat({ start: 10, step: 2, done: 20 }, function(i) {
console.log(i);
}).then(function() {
console.log('done');
});
// 10
// 12
// 14
// 16
// 18
// 'done'
async / await
を使ってループする例:
この例は /api/users/0
から api/users/9
までのユーザーデータを出力し、最後に 'done' を出力します。
async function getUser(userId) {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
// chillout.repeatのコールバックでasync functionを渡す
async function logUsers() {
await chillout.repeat(10, async i => {
const user = await getUser(i);
console.log(user);
});
console.log('done');
}
logUsers();
与えられた関数 callback
を、 chillout.StopIteration
が返されるかエラーが発生するまで繰り返します。
このメソッドは JavaScript の while (true) { ... }
ステートメントのように使えます。
- chillout.until ( callback [, context ] )
- @param {function} callback 各ループに対して実行するコールバック関数。
- @param {object} [context] 任意。コールバック内で
this
として使用する値。 - @return {promise} Promiseオブジェクトを返します。
var i = 0;
chillout.until(function() {
console.log(i);
i++;
if (i === 5) {
return chillout.StopIteration; // ループを止める
}
}).then(function() {
console.log('done');
});
// 0
// 1
// 2
// 3
// 4
// 'done'
async / await
を使ってループする例:
この例はファイルの変更を監視して、変更された内容を出力します。
// ミリ秒(msec)が過ぎるまで待機する関数
function sleep(msec) {
return new Promise(resolve => setTimeout(resolve, msec));
}
async function getFileContents(url) {
const response = await fetch(url);
return response.text();
}
let previous = null;
// chillout.untilのコールバックでasync functionを渡す
async function logNewFileContents() {
await chillout.until(async () => {
const contents = await getFileContents('./file1.txt');
if (previous === null) {
previous = contents;
}
if (contents !== previous) {
console.log('file changed!');
previous = contents;
return chillout.StopIteration; // ループを止める
}
await sleep(1000);
});
console.log(previous);
}
logNewFileContents();
与えられた関数 callback
を、 chillout.StopIteration
が返されるかエラーが発生するまで繰り返します。
このメソッドは JavaScript の while (true) { ... }
ステートメントのように使え、 until
と同じ動作をしますが、CPU に負荷がかからないよう until
よりゆっくり実行します。
このメソッドは、何らかの処理が終わるまで待ちたいときに向いています。
- chillout.waitUntil ( callback [, context ] )
- @param {function} callback 各ループに対して実行するコールバック関数。
- @param {object} [context] 任意。コールバック内で
this
として使用する値。 - @return {promise} Promiseオブジェクトを返します。
chillout.waitUntil(function() {
// body要素が読み込まれるまで待機する
if (document.body) {
return chillout.StopIteration; // ループを止める
}
}).then(function() {
document.body.innerHTML += 'body loaded';
});
何らかの処理が終わるまで待つ例:
someProcessing();
chillout.waitUntil(function() {
if (isSomeProcessingDone) {
return chillout.StopIteration; // break loop
}
}).then(function() {
nextProcessing();
});
列挙可能なプロパティに対して、ループ処理を行います。
これは for-of
ステートメントと同じループ処理をします。
与えられた関数 callback
を各ループに対して実行します。
関数内で chillout.StopIteration
を返すか、エラーが発生すると、それ以降のループは実行されません。
- chillout.forOf ( iterable, callback [, context ] )
- @param {array|string|object} iterable 列挙可能なプロパティに対して、ループ処理を行うオブジェクト。
- @param {function} callback 各ループに対して実行するコールバック関数で、1つの引数をとります。
- value: 各ループ処理におけるプロパティの値。
- @param {object} [context] 任意。コールバック内で
this
として使用する値。 - @return {promise} Promiseオブジェクトを返します。
配列のループ例:
chillout.forOf([1, 2, 3], function(value) {
console.log(value);
}).then(function() {
console.log('done');
});
// 1
// 2
// 3
// 'done'
文字列のループ例:
chillout.forOf('abc', function(value) {
console.log(value);
}).then(function() {
console.log('done');
});
// 'a'
// 'b'
// 'c'
// 'done'
既存のJavaScriptループを chillout.js のAPIに置き換えるとCPU負荷を軽減することができます。
変換例:
JavaScript Statement | chillout |
---|---|
[1, 2, 3].forEach(function(v, i) {}) | chillout.forEach([1, 2, 3], function(v, i) {}) |
for (i = 0; i < 5; i++) {} | chillout.repeat(5, function(i) {}) |
for (i = 10; i < 20; i += 2) {} | chillout.repeat({ start: 10, step: 2, done: 20 }, function(i) {}) |
while (true) {} | chillout.until(function() {}) |
while (cond()) { doSomething(); } |
chillout.until(function() { if (!cond()) return chillout.StopIteration; doSomething(); }) |
for (value of [1, 2, 3]) {} | chillout.forOf([1, 2, 3], function(value) {}) |
※ async/await
が使える環境ではより簡潔に書けます。 chillout.js のAPIはすべて Promise を返すので async/await
で扱えるようになっています。
バグレポートや機能要望などの Issues や、Pull Request を歓迎しています。
Pull Requestの際は、 npm run test
を実行してエラーがないことを確認していただけると助かります。
MIT