ページ上でずっと動いているsetTimeout、setInterval、requestAnimationFrameを見つけてパフォーマンス改善する
複雑なウェブアプリケーションになってくると、1つのページで複数のTimerなどを回すことがあります。
例えば、Twitterのようなアプリならば、ポーリングで更新するためにsetInvervalのようなタイマーを回します。
また、ゲームなどCanvasで描画を行うアプリケーションならば、メインループをrequestAnimationFrameで回します。
このように色々なタイマー系が一つのアプリで動くことが多いですが、特に問題がなりやすいのが表示中だけタイマーを回すコンポーネントです。
よくあるのが次のようなmount時にtimerを開始して、unmount時にtimerを停止するコンポーネントです。
この実装はunmount時に止めているので問題ありませんが、componentWillUnmountの実装を忘れるとそのタイマーはコンポーネントが消えた後も回り続けます。
export class TimerComponent extends React.Component {
componentDidMount() {
this.startTimer();
}
componentWillUnmount() {
this.stopTimer(); // <= これを止め忘れるとTimerリーク
}
}
このような意図しないで動いてるタイマーなどを見つけるspyスクリプトを書きました
使い方
- 次のスクリプトをページに読み込ませる
- コンソールにコピペして実行しても大丈夫
- Non strict modeじゃないと動かないことやってるで混ぜる場合は注意
- 結果を取りたくなったら
window.getContexualLogResult()を叩く
"setTimeout", "setInterval", "requestAnimationFrame"の実行元の関数毎に呼ばれた回数をまとめて表示してくれます。 またスタックトレースも無理やり入れているので、意図しない呼び出しが頻発しているならその部分のコードを直す目安となります。
| /** | |
| * ## Usage | |
| * | |
| * 1. Load following script | |
| * 2. `window.getContexualLogResult()` output the result to console | |
| * | |
| * ## Description | |
| * | |
| * - It spy "setTimeout", "setInterval", and "requestAnimationFrame". | |
| * - Collect call count and collect stack trace. | |
| */ | |
| // Disapprear log less than 5 | |
| const thresholdCount = 5; | |
| const map = new Map([ | |
| ["setTimeout", {}], | |
| ["setInterval", {}], | |
| ["requestAnimationFrame", {}] | |
| ]); | |
| window.getContexualLogResult = () => { | |
| ["setTimeout", "setInterval", "requestAnimationFrame"].forEach(name => { | |
| const result = Object.entries(map.get(name)) | |
| .filter(entry => { | |
| return entry[1] > thresholdCount; | |
| }).map(entry => { | |
| return { | |
| code: entry[0], | |
| count: entry[1] | |
| } | |
| }); | |
| console.group(name); | |
| console.table(result); | |
| console.groupEnd(name); | |
| }); | |
| } | |
| const contexualLog = (type, log) => { | |
| if (map.has(type)) { | |
| const object = map.get(type); | |
| const count = object[log] ? object[log] : 0; | |
| object[log] = count + 1; | |
| } else { | |
| const object = {}; | |
| const count = object[log] ? object[log] : 0; | |
| object[log] = count + 1; | |
| map.set(type, object); | |
| } | |
| } | |
| const originalTimout = window.setTimeout; | |
| window.setTimeout = function () { | |
| const stack = (new Error().stack).split("\n").slice(1); | |
| if (arguments.callee.caller) { | |
| contexualLog("setTimeout", arguments.callee.caller.toString() + "\n" + stack); | |
| } else { | |
| contexualLog("setInterval", stack) | |
| } | |
| return originalTimout.apply(window, arguments); | |
| } | |
| const originalInterval = window.setInterval; | |
| window.setInterval = function () { | |
| const stack = (new Error().stack).split("\n").slice(1).join("\n"); | |
| if (arguments.callee.caller) { | |
| contexualLog("setInterval", arguments.callee.caller.toString() + "\n" + stack); | |
| } else { | |
| contexualLog("setInterval", stack) | |
| } | |
| return originalInterval.apply(window, arguments); | |
| } | |
| const originalRaF = window.requestAnimationFrame; | |
| window.requestAnimationFrame = function (callback) { | |
| const stack = (new Error().stack).split("\n").slice(1).join("\n"); | |
| if (arguments.callee.caller) { | |
| contexualLog("requestAnimationFrame", arguments.callee.caller.toString() + "\n" + stack); | |
| } else { | |
| contexualLog("requestAnimationFrame", stack) | |
| } | |
| return originalRaF(callback); | |
| } |
例えば、twtter.comでこれを実行してみるとsetIntervalとrequestAnimationFrameが回っていることが分かります。
これは定期的な更新をするために呼び出していることがわかります。
タイムラインツールでも記録はできるのですが、呼び出し元毎のグルーピングやフィルタリングが難しいです。(良い方法があるなら知りたい) "setTimeout", "setInterval", "requestAnimationFrame"を乗っ取ってログを取ることで実装しています。
一回のタイマー発火ごとの処理は小さくても、スペック弱いデバイスではネックとなることがあるのでそのような無駄な処理を発見する目的で作りました。 (ChromeのCPU Throttlingなどでシミュレートすると問題を見つけやすいです)
最近は、分かりやすい指標が既にある起動時間のパフォーマンスではなく、アプリを起動後のパフォーマンスを改善しています。
次の記事で作ってたものはそういうところを改善する目安を探すためのツールです。
- performance.markにメタデータを紐付けできるライブラリを書いた | Web Scratch
performance.markwith metadata is useful for Real user monitoring
アプリの起動後の指標として、何かした時に反応が100ms以内、アニメーションが10ms以内、アイドル時の処理は50ms以上以内のブロックにする(long task)、Loadは1000ms以内などを指標を定めたRAILモデルなどがあります。
これらはマイクロなベンチマークを取ってからそれを改善していくという積み重ねをしています。 この記事で書いたspyスクリプトも、無駄に動くタイマーが減ればその分処理が減ったということが明確であるため、それを検出するために作りました。
また、アプリ起動後は何もしてないときも体感が良いということも必要になります。 例えばユーザー操作がないけど、タイムラインがスムーズに更新される、映像がスムーズに流れる、放置ゲームを眺めててつっかかりがないとか、リアルタイムにデータを受信してて止まらないなどがこれにあたります。
これらの放置時の更新は大体裏では"setTimeout", "setInterval", "requestAnimationFrame"などを使っていることが多いです。 (WebRTCやWebSocketなどもありますが、それらが止まってないかを定期的にチェックする仕組みなどにも関係します)
タイマー系は意図しないタイミングで他の処理と重なるとUIを固めたりするので、requestIdleCallbackと組み合わせるなどの工夫が必要になるかもしれません。
その他
お知らせ欄
次に書くかもしれない記事候補
興味がありましたらIssues · efcl/efcl.github.ioからご意見下さい