- 投稿日:
ブラウザのタブがバックグラウンド状態になっているとJSの実行が止まることがあり、そうするとsetInterval()
が死ぬので、これを回避する技。
結論としてはWeb Worker APIのWorkerインターフェースを使うことで解決できる。
確認環境
Env | ver |
---|---|
Microsoft Edge | 126.0.2592.87 |
サンプルコード
/**
* @param {() => void} cb 実行するコールバック
* @param {number} interval 実行間隔
* @returns 停止用の関数
* */
const createWorkerInterval = (cb, interval) => {
const src = "self.addEventListener('message', (msg) => { setInterval(() => self.postMessage(null), msg.data) })";
const wk = new Worker(`data:text/javascript;base64,${btoa(src)}`);
wk.onmessage = () => cb();
wk.postMessage(interval);
return wk.terminate;
}
- 投稿日:
Node.jsとGoogle Chrome, Microsoft Edgeを用いて、try-catchとif文でエラー処理にかかる時間がどのくらい違うのかを調べた。
計測手法
次の4パターンを100万回実行した結果を記載している。
- Result型としてエラーオブジェクトをreturnし、if文で判定する方式
- Result型としてエラーインスタンスをreturnし、if文で判定する方式
- エラーオブジェクトをthrowし、try-catchで判定する方式
- エラーインスタンスをthrowし、try-catchで判定する方式
確認に使用したコード:https://gist.github.com/Lycolia/304bc9e825e821c2d582f3ef9f700817
計測結果
CPUによって処理速度がかなり変動するが、いずれの環境でも処理速度の速さは以下の通り。
- Result型としてエラーオブジェクトをreturnし、if文で判定する方式
- エラーオブジェクトをthrowし、try-catchで判定する方式
- Result型としてエラーインスタンスをreturnし、if文で判定する方式
- エラーインスタンスをthrowし、try-catchで判定する方式
計測に使用したCPU
CPU | コア | スレッド | クロック | L2キャッシュ | L3キャッシュ |
---|---|---|---|---|---|
Intel Core i7 13700 | 16 | 24 | 5.20GHz | 24.00MB | 30.00MB |
Intel Core i7 1265U | 10 | 12 | 4.80GHz | 6.50MB | 12.00MB |
AMD Ryzen 5 5600G | 6 | 12 | 3.90GHz | 3.00MB | 16.00MB |
Intel Core i7 13700端末
16C24T, 5.20GHz, L2 24.00MB, L3 30.00MB。ミドルタワーPC。
Node.js v16.20.2
処理環境
Env | Ver |
---|---|
CPU | Intel Core i7 13700 |
OS | Ubuntu 20.04.6 LTS (WSL2) |
処理結果
処理方式 | 処理時間(ms) |
---|---|
Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 802 |
Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 5,942 |
エラーオブジェクトをthrowし、try-catchで判定する方式 | 1,847 |
エラーインスタンスをthrowし、try-catchで判定する方式 | 6,527 |
Node.js v18.19.1
処理環境
Env | Ver |
---|---|
CPU | Intel Core i7 13700 |
OS | Ubuntu 20.04.6 LTS (WSL2) |
処理結果
処理方式 | 処理時間(ms) |
---|---|
Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 887 |
Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 5,826 |
エラーオブジェクトをthrowし、try-catchで判定する方式 | 2,006 |
エラーインスタンスをthrowし、try-catchで判定する方式 | 6,464 |
Node.js v20.11.1
処理環境
Env | Ver |
---|---|
CPU | Intel Core i7 13700 |
OS | Ubuntu 20.04.6 LTS (WSL2) |
処理結果
処理方式 | 処理時間(ms) |
---|---|
Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 720 |
Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 5,375 |
エラーオブジェクトをthrowし、try-catchで判定する方式 | 1,690 |
エラーインスタンスをthrowし、try-catchで判定する方式 | 5,978 |
Microsoft Edge 121.0.2277.128
処理環境
Env | Ver |
---|---|
CPU | Intel Core i7 13700 |
OS | Windows 11 Pro 22H2(22621.3155) |
処理結果
処理方式 | 処理時間(ms) |
---|---|
Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 4,246 |
Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 12,329 |
エラーオブジェクトをthrowし、try-catchで判定する方式 | 11,985 |
エラーインスタンスをthrowし、try-catchで判定する方式 | 14,105 |
Google Chrome 122.0.6261.58
処理環境
Env | Ver |
---|---|
CPU | Intel Core i7 13700 |
OS | Windows 11 Pro 22H2(22621.3155) |
処理結果
処理方式 | 処理時間(ms) |
---|---|
Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 3,507 |
Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 10,793 |
エラーオブジェクトをthrowし、try-catchで判定する方式 | 10,482 |
エラーインスタンスをthrowし、try-catchで判定する方式 | 12,452 |
Intel Core i7 1265U端末
10C12T, 4.80GHz, L2 6.50MB, L3 12.00MB。ノートPC。
Node.js v16.20.2
処理環境
Env | Ver |
---|---|
CPU | Intel Core i7 1265U |
OS | Ubuntu 20.04.6 LTS (WSL2) |
処理結果
処理方式 | 処理時間(ms) |
---|---|
Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 972 |
Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 11,416 |
エラーオブジェクトをthrowし、try-catchで判定する方式 | 2,288 |
エラーインスタンスをthrowし、try-catchで判定する方式 | 13,993 |
Node.js v18.19.1
処理環境
Env | Ver |
---|---|
CPU | Intel Core i7 1265U |
OS | Ubuntu 20.04.6 LTS (WSL2) |
処理結果
処理方式 | 処理時間(ms) |
---|---|
Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 1,102 |
Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 11,333 |
エラーオブジェクトをthrowし、try-catchで判定する方式 | 2,441 |
エラーインスタンスをthrowし、try-catchで判定する方式 | 12,827 |
Node.js v20.11.1
処理環境
Env | Ver |
---|---|
CPU | Intel Core i7 1265U |
OS | Ubuntu 20.04.6 LTS (WSL2) |
処理結果
処理方式 | 処理時間(ms) |
---|---|
Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 888 |
Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 10,881 |
エラーオブジェクトをthrowし、try-catchで判定する方式 | 2,269 |
エラーインスタンスをthrowし、try-catchで判定する方式 | 12,254 |
Microsoft Edge 121.0.2277.128
処理環境
Env | Ver |
---|---|
CPU | Intel Core i7 1265U |
OS | Windows 11 Pro 22H2(22621.3155) |
処理結果
処理方式 | 処理時間(ms) |
---|---|
Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 7,526 |
Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 34,761 |
エラーオブジェクトをthrowし、try-catchで判定する方式 | 39,005 |
エラーインスタンスをthrowし、try-catchで判定する方式 | 65,752 |
Google Chrome 122.0.6261.58
処理環境
Env | Ver |
---|---|
CPU | Intel Core i7 1265U |
OS | Windows 11 Pro 22H2(22621.3155) |
処理結果
処理方式 | 処理時間(ms) |
---|---|
Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 6,303 |
Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 16,460 |
エラーオブジェクトをthrowし、try-catchで判定する方式 | 17,979 |
エラーインスタンスをthrowし、try-catchで判定する方式 | 23,378 |
AMD Ryzen 5 5600G端末
6C12T, 3.90GHz, L2 3.00MB, L3 16.00MB。ミニタワーPC。
この端末はOSがUbuntuであるため、これまでのWindows環境との単純比較はできない。
Node.js v16.20.2
処理環境
Env | Ver |
---|---|
CPU | AMD Ryzen 5 5600G |
OS | Ubuntu 22.04.3 LTS |
処理結果
処理方式 | 処理時間(ms) |
---|---|
Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 1,786 |
Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 11,242 |
エラーオブジェクトをthrowし、try-catchで判定する方式 | 3,880 |
エラーインスタンスをthrowし、try-catchで判定する方式 | 12,716 |
Node.js v18.19.1
処理環境
Env | Ver |
---|---|
CPU | AMD Ryzen 5 5600G |
OS | Ubuntu 22.04.3 LTS |
処理結果
処理方式 | 処理時間(ms) |
---|---|
Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 1,933 |
Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 11,094 |
エラーオブジェクトをthrowし、try-catchで判定する方式 | 3,839 |
エラーインスタンスをthrowし、try-catchで判定する方式 | 12,767 |
Node.js v20.11.1
処理環境
Env | Ver |
---|---|
CPU | AMD Ryzen 5 5600G |
OS | Ubuntu 22.04.3 LTS |
処理結果
処理方式 | 処理時間(ms) |
---|---|
Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 1,714 |
Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 10,741 |
エラーオブジェクトをthrowし、try-catchで判定する方式 | 3,444 |
エラーインスタンスをthrowし、try-catchで判定する方式 | 11,978 |
Microsoft Edge 121.0.2277.128
処理環境
Env | Ver |
---|---|
CPU | AMD Ryzen 5 5600G |
OS | Ubuntu 22.04.3 LTS |
処理結果
処理方式 | 処理時間(ms) |
---|---|
Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 4,348 |
Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 21,584 |
エラーオブジェクトをthrowし、try-catchで判定する方式 | 15,543 |
エラーインスタンスをthrowし、try-catchで判定する方式 | 23,119 |
Google Chrome 122.0.6261.57
これだけバージョンが合わなかった。
処理環境
Env | Ver |
---|---|
CPU | AMD Ryzen 5 5600G |
OS | Ubuntu 22.04.3 LTS |
処理結果
処理方式 | 処理時間(ms) |
---|---|
Result型としてエラーオブジェクトをreturnし、if文で判定する方式 | 4,842 |
Result型としてエラーインスタンスをreturnし、if文で判定する方式 | 16,607 |
エラーオブジェクトをthrowし、try-catchで判定する方式 | 15,051 |
エラーインスタンスをthrowし、try-catchで判定する方式 | 18,095 |
全体サマリ表
凡例
- Result型としてエラーオブジェクトをreturnし、if文で判定する方式
- Result型としてエラーインスタンスをreturnし、if文で判定する方式
- エラーオブジェクトをthrowし、try-catchで判定する方式
- エラーインスタンスをthrowし、try-catchで判定する方式
CPUによってかなり差が出ているが、Intel CPUの13700と1265Uを比較した場合、例外を作らない1, 3のケースでは処理に大きな差がないが、例外を作る2, 4のケースでは有意な差が認められるので、モバイル向けとデスクトップ向けでCPUの処理効率に差があることがわかる。
AMD CPUと比較した場合、Node.js環境で例外を作らないケースではIntel CPUに大きく引けを取るが、ブラウザ上でのそれではパフォーマンスは高い。また例外を作るケースでもNode.js・ブラウザ共にIntel CPUと比較した場合のパフォーマンスが比較的よいという、面白い結果になっている。但しOSが違うことの影響もあるため、単純比較はできないが…。
あとがき
ひとまずtry-catchを使うと遅くなるということが確かめられたので良かったし、環境によって劇的遅くなるケースがあるというのは思わない収穫だった。
これを試そうと思った切欠はtry-catchが乱用されているコードを見てパフォーマンス劣化に繋がっているのではないかという疑問を抱いたからだ。try-catchでパフォーマンスの劣化が起きることは知識としても経験としても知っていたのだが、ちゃんとレポートしたことがなかったので今回まとめてみた。
try-catchの利用によりパフォーマンスの劣化が起きるというのは、古い時代にあった開発のお作法を知っている人であれば、それなりに常識だとは思うが、最近は知られていないか、知られていても意識していないことが少なくないと思われるので、今回記事にしてみた感じだ。
100万回の実行なので細かい話になるとは思うがパフォーマンスチューニングは細かいことの積み重ねでもあるので、むやみやたらに例外を使わないことは重要だと思う。そもそも例外は制御しづらいので、可能な限りif文で処理を書くことが望ましいだろう。
処理が遅延する原因としては二つあると考えていて、一つは例外生成時に顕著であることからスタックトレースの生成を初めとしたエラーオブジェクトの生成に時間がかかっているのと、もう一つはcatchでも遅延が出ることから、コールスタックから最寄りのcatchを辿るのに時間を取られていると思われる。今回の検証では特に出してないが、以前確認した時の記憶が確かであれば、catchに入らない限り、tryだけで顕著に処理が遅延することはなかったと思う。
あと、どうでもいいことだがconsole.log()
で結果を出したときにEdgeだけObjectのKeyの順序が他と違ったので転記しているときに微妙に不便だった。何回やっても同じだったので恐らくEdgeだけキーをソートするアルゴリズムが違うのだと思う。まぁObjectとかHashとかMapとかDictionaryみたいなやつは順序が保証されないので別にいいっちゃいいんだけど、まさか実装によってソート方式が違うというのは思いもしなかった。
EdgeとChromeで処理結果が優位に違うのも、恐らくこれが原因なのだろう。分からないが多分JSのエンジンの実装が違う気がする。
それとLinuxのEdgeにはマウスジェスチャー機能がないという悲しいことも分かった。
参考
- 投稿日:
adiaryの改造でやったのでメモがてら
環境
Env | Ver |
---|---|
Microsoft Edge | 120.0.2210.144 |
PHP | 8.2.10 |
サンプルコード
FORM形式での送信
Web標準でサポートされているため実装が非常に容易。特に理由がなければこれでいい
HTMLコード
<textarea onpaste="handlePasteForm(event)"></textarea>
JSコード
fetch()
のヘッダ指定がないが、 fetch()
を使う場合はFormDataを使うと勝手に生えるので気にしなくていい。他にもXMLHttpRequest.send()
やNavigator.sendBeacon()
でも生えるらしい。
今回試したEdgeではバウンダリー文字列もちゃんと生えていた。
/** @param {ClipboardEvent} e */
const handlePasteForm = (e) => {
if (e.clipboardData.files.length) {
const pasteFile = e.clipboardData.files[0];
const form = new FormData();
form.append('image', pasteFile);
fetch('./test.php', {
method: 'POST',
body: form
});
} else {
// 何もしない
}
};
PHPコード
Content-Type
がmultipart/form-data
の時だけ中身が入ってくる
参考:https://www.php.net/manual/ja/reserved.variables.files.php
<?php
$file_name = $_FILES['image']['name'];
move_uploaded_file($_FILES['image']['tmp_name'], './'. $file_name);
>
$_FILES
の中身はvar_dumpした限りこんな感じだった
array(1) {
["image"]=>
array(6) {
["name"]=>
string(9) "image.png"
["full_path"]=>
string(9) "image.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(29) "C:\env\msys64\tmp\php3B90.tmp"
["error"]=>
int(0)
["size"]=>
int(3988)
}
}
JSON形式での送信
Web標準ではないので、これといったやり方もなく、正直面倒だが、どうしてもJSONで送らないといけないときに。
HTMLコード
<textarea onpaste="handlePasteJson(event)"></textarea>
JSコード
HTTPではバイナリを送ることができないため、Base64にエンコードして送る。Base64エンコードに含まれる情報はファイルバイナリのみ。もしFORM送信のようにファイル名も送りたければ、別途送ってやる必要がある。
/** @param {File} file */
const encodeBase64 = (file) => {
return new Promise ((resolve, reject) => {
const fr = new FileReader();
fr.onload = () => {
const data = fr.result.split(',')[1];
resolve(fr.result.split(',')[1]);
};
fr.onerror = (ev) => {
reject(ev);
};
fr.readAsDataURL(file);
});
};
/** @param {ClipboardEvent} e */
const handlePasteJson = async (e) => {
if (e.clipboardData.files.length) {
const pasteFile = e.clipboardData.files[0];
const base64File = await encodeBase64(pasteFile);
fetch('./test.php', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ image: base64File })
});
} else {
// 何もしない
}
};
PHPコード
JSONリクエストを取るときはfile_get_contents('php://input')
を利用する。$_POST
はContent-Type
がapplication/x-www-form-urlencoded
ないしmultipart/form-data
の時しか取れないからだ。つまりFORMリクエスト以外はfile_get_contents('php://input')
で取ると覚えておけばよいだろう。
参考:https://www.php.net/manual/ja/wrappers.php.php#wrappers.php.input
以下のコードはかなりいい加減なので余りアテにしてはいけない(特に拡張子取得の下りがガバガバすぎる)が、取り合えずpngかjpegかgifが送られてきた場合は動いてくれる筈だ。
<?php
// 連想配列にするため、json_decode()の第二引数をtrueにする(指定しないとstdClassになる
$json = json_decode(file_get_contents('php://input'), true);
// Base64エンコードを解除する
$file = base64_decode($json['image']);
// 一時ファイルに落とす
file_put_contents('json_upload', $file);
// 拡張子判別
$mime = mime_content_type('json_upload');
// 拡張子取得
$ext = explode('/', $mime);
// 拡張子付きにリネーム
rename('json_upload', 'image.'. $ext[1]);
参考
JSONでファイルを投げる方式での実装状況については以下の記事が参考になる。
WebAPI でファイルをアップロードする方法アレコレ #API - Qiita
一気通貫で動くサンプルコード
今までの実装が全部動かせるサンプル
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if(preg_match('/^multipart\/form-data;/', $_SERVER['CONTENT_TYPE'])) {
// form
$file_name = $_FILES['image']['name'];
move_uploaded_file($_FILES['image']['tmp_name'], './'. $file_name);
} else {
// json
$json = json_decode(file_get_contents('php://input'), true);
$file = base64_decode($json['image']);
file_put_contents('json_upload', $file);
$mime = mime_content_type('json_upload');
$ext = explode('/', $mime);
rename('json_upload', 'image.'. $ext[1]);
}
} else {
?>
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>Upload test</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<script>
/** @param {File} file */
const encodeBase64 = (file) => {
return new Promise ((resolve, reject) => {
const fr = new FileReader();
fr.onload = () => {
const data = fr.result.split(',')[1];
resolve(fr.result.split(',')[1]);
};
fr.onerror = (ev) => {
reject(ev);
};
fr.readAsDataURL(file);
});
};
/** @param {ClipboardEvent} e */
const handlePasteForm = (e) => {
if (e.clipboardData.files.length) {
const pasteFile = e.clipboardData.files[0];
const form = new FormData();
form.append('image', pasteFile);
fetch('./test.php', {
method: 'POST',
body: form
});
} else {
// 何もしない
}
};
/** @param {ClipboardEvent} e */
const handlePasteJson = async (e) => {
if (e.clipboardData.files.length) {
const pasteFile = e.clipboardData.files[0];
const base64File = await encodeBase64(pasteFile);
fetch('./test.php', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ image: base64File })
});
} else {
// 何もしない
}
};
</script>
</head>
<body>
<div>Form:<textarea onpaste="handlePasteForm(event)"></textarea></div>
<div>Json:<textarea onpaste="handlePasteJson(event)"></textarea></div>
</body>
</html>
<?php
}
?>
- 投稿日:
EdgeとChromeは同じだったがNode.jsでは異なる挙動をしていたのでそのメモ
確認環境
Env | ver |
---|---|
Microsoft Edge | 120.0.2210.9 |
Google Chrome | 120.0.6099.130 |
Node.js | 20.0.0 |
環境別の確認結果
Edge
DevToolsで確認
const err = new Error('test');
Object.getPrototypeOf(err);
// {name: 'Error', message: '', constructor: ƒ, toString: ƒ}
Google Chrome
DevToolsで確認
const err = new Error('test');
Object.getPrototypeOf(err);
// {name: 'Error', message: '', constructor: ƒ, toString: ƒ}
Node.js
node -i
で確認
const err = new Error('test');
Object.getPrototypeOf(err);
// {}
- 投稿日:
ESM化が叫ばれて久しいですが、未だにJestはESMとCJSが混在したコードを処理してくれません。
Getting StartedにもSWCに対する言及がないので、きっともう忘れられているのでしょう。swc-project/jestの方も特にやる気はなさそうだし、やりたければ自分でPR書きましょうって感じだと思います。きっと。
確認環境
node_modules配下にESMで作られたモジュールが存在し、コードはTypeScript、トランスパイルにはSWCを利用する。
Env | Ver |
---|---|
@swc/cli | 0.1.62 |
@swc/core | 1.3.92 |
@swc/jest | 0.2.29 |
@swc/register | 0.1.10 |
jest | 29.7.0 |
typescript | 5.2.2 |
やったけど意味がなかったこと
package.json
のtype
をmodule
にするjest.config.js
のtransformIgnorePatterns
にESMモジュールのパスだけ除外する設定を書く- 上記に加えて
transform.jsc.path
にpkg-name: ['node_modules/pkg-name']
を追記する node --experimental-vm-modules node_modules/jest/bin/jest.js
で実行する- 多少マシになったがコケるものはコケる
- ESMで書かれたモジュールを丸ごとモックする
- 一切効果なし
所感
多分Webpackでバンドルしてnode_modules
の中身も外も関係ない状態にするのが一番無難なのではないかと思いました。
Node.jsの組み込みテストランナーにすれば解決するかな?と思ったものの、こちらは現状SWCでは使えそうにないので諦めました。
参考までに以下のコマンドで走らさせられます。
node --require @swc/register --test ./src/**/*.spec.ts
取り敢えずESMに引っかったモジュールはCJS時のバージョンを維持しておくことにしましたが、このままだとSWC使えないし、なんとかなって欲しいですね。Webpack使えば解決できるのはわかるんですが、このために使いたくないので、テストを重視する場合、Vitestを持つViteが有力候補になって来そうです。
2024-02-17追記
esbuild + Node.js built-in test runnerの組み合わせであればテストはできるが肝心の実行ができず無意味だった