Skip to content

Latest commit

 

History

History
509 lines (377 loc) · 17.5 KB

README-ja.md

File metadata and controls

509 lines (377 loc) · 17.5 KB

chillout.js

重いループのCPU負荷を軽減し、体感的・心理的に高速化するJavaScriptライブラリ。

NPM Version Build Status Bundle Size (minified) GitHub License

Table of contents

概要

chillout.js は「処理時間を短くする」という物理的な高速化とは違い、CPU負荷を軽減しリソースに余裕を持たせ、重い処理でも軽く感じさせることでユーザーにとって体感的・心理的な高速化につなげる JavaScript ライブラリです。

重い処理から開放されるために

重いWebページやゲームでは画面が読み込み中のままカクカクだったり、Webページに限らずnode.jsでバッチ処理するときでも高CPU負荷が続くとマシンごと重くなってしまいます。

特にスペックの低い端末では、CPU使用率100%の状態が続くと加熱されて冷却ファンが激しく回り、そのまま使い続けると冷却が追いつかずに熱暴走してしまう可能性もあります。

JavaScriptでCPU負荷を軽減するには?

重い処理のほとんどはループ処理によって発生します。ループの中でさらにループ、その中でさらにループ…。単純に考えた場合、そうならないようループの途中で一定時間処理を休止させればいいんですが、それができません。
JavaScript には一定の時間休む sleep のような機能がないからです。そこで、sleepするにはどうするか?というと「非同期」でループ処理します。

JavaScript では、setTimeoutprocess.nextTick を使って同期処理を非同期化できます。 それと Promise を組み合わせると sleep のように一定時間CPUを休ませる非同期処理が実現できます。

処理時間を短くするんじゃなく、体感速度を向上させる体感的・心理的な高速化

処理の高速化というと、とにかく1ミリ秒でも処理時間を短くすることが手法とされますが、Webページやアプリ、ゲームといった、人が画面を見たり操作する場合 心理的に「速い」と感じればユーザーのストレスが減り結果として高速化につながります。

chillout.js は、ループ処理が重いときにはCPUが休まるくらいの休止時間、処理が速いときには休止時間なしか、わずかな休止時間をいれ本来のループを邪魔しないようにし、結果としてカクカクするような重さを感じさせずにループを実行します。

ほとんどの場合、既存の JavaScript ループの代わりに chillout.js のループを使うことで CPU 使用率を減らすことができます。非同期ループとして扱う必要がありますが async/await を使うと扱いやすくなります。

また、処理が重くなるとでる 「警告: 応答のないスクリプト」 といったブラウザ警告なしでJavaScriptを実行できます。

ブラウザ上、Electron、Node.js などの環境で使えます。

インストール

npm

$ npm install chillout

CDN

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

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;
}

forループ

var time = Date.now();
for (var i = 0; i < 1000; i++) {
  heavyProcess();
}
var processingTime = Date.now() - time;
console.log(processingTime);

CPU usage without chillout

  • 処理時間: 107510ms.
  • CPU平均使用率(Nodeプロセス): 97.13%

chillout.repeat

var time = Date.now();
chillout.repeat(1000, function(i) {
  heavyProcess();
}).then(function() {
  var processingTime = Date.now() - time;
  console.log(processingTime);
});

CPU usage with chillout

  • 処理時間: 138432ms.
  • CPU平均使用率(Nodeプロセス): 73.88%

ベンチマーク結果

CPU usage with chillout

  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 でベンチマークを実行できます。


API

forEach

与えられた関数 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();

repeat

与えられた関数 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オブジェクトを返します。

回数を指定する例:

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();

until

与えられた関数 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();

waitUntil

与えられた関数 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();
});

forOf

列挙可能なプロパティに対して、ループ処理を行います。 これは 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 で扱えるようになっています。

Pull Request

バグレポートや機能要望などの Issues や、Pull Request を歓迎しています。 Pull Requestの際は、 npm run test を実行してエラーがないことを確認していただけると助かります。

ライセンス

MIT