aboutsummaryrefslogtreecommitdiff
path: root/files/ja/tools/performance/scenarios/intensive_javascript/index.html
blob: 34cc1db6726e3fc3630482451cc28e457345eb7f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
---
title: 集約的な JavaScript
slug: Tools/Performance/Scenarios/Intensive_JavaScript
translation_of: Tools/Performance/Scenarios/Intensive_JavaScript
---
<div>{{ToolsSidebar}}</div><div class="summary">
<p>デフォルトでブラウザはレイアウト、リフロー、ガベージコレクションだけでなく、ページ内のすべての JavaScript もひとつのスレッドで実行します。これは長い間実行する JavaScript がスレッドをブロックして、ページの不応答やユーザエクスペリエンスの悪化を招くおそれがあるということです。</p>

<p><a href="/ja/docs/Tools/Performance/Frame_rate">フレームレート</a>および<a href="/ja/docs/Tools/Performance/Waterfall">ウォーターフォール</a>ツールを使用して、いつ JavaScript がパフォーマンスの問題を起こしているかを知る、および特に注意が必要な関数を選び出すことができます。</p>

<p>本記事では長い間実行する JavaScript が応答性の問題を起こしているサンプルサイトを使用して、問題を修正するために 2 種類の方法を適用していきます。ひとつは長い間実行する JavaScript を複数の部品に分けて、それらのスケジューリングに <code><a href="/ja/docs/Web/API/window/requestAnimationFrame">requestAnimationFrame</a></code> を使用する方法、もうひとつは <a href="/ja/docs/Web/API/Web_Workers_API/Using_web_workers">web worker</a> を使用して関数全体を別のスレッドに分ける方法です。</p>
</div>

<p>自身でも試してみたい場合は、デモ Web サイトが<a class="external external-icon" href="http://mdn.github.io/performance-scenarios/js-worker/index.html">こちら</a>にあります。</p>

<p>動画版のウォークスルーも用意しています:</p>

<p>{{EmbedYouTube("Pcc6jQX6JDI")}}</p>

<p>デモ Web サイトは以下のようなものです:</p>

<p><img alt="" src="https://mdn.mozillademos.org/files/11031/js-worker-demo.png" style="display: block; height: 677px; margin-left: auto; margin-right: auto; width: 1000px;">ここには 3 つのコントロールがあります:</p>

<ul>
 <li>JavaScript の実行方法を制御するラジオボタン。ブロックが発生するひとつの操作をメインスレッドで実行する、<code>requestAnimationFrame()</code> を使用して小規模な操作の集まりをメインスレッドで実行する、Worker を使用して別のスレッドで実行する。</li>
 <li>"Do pointless computations!" と記載された、JavaScript を実行するボタン。</li>
 <li>CSS アニメーションを開始・終了するボタン。これはブラウザに、バックグラウンドで実行するタスクを与えます。</li>
</ul>

<p>ラジオボタンで "Use blocking call in main thread" を選択して、記録を始めましょう:</p>

<ul>
 <li>"Start animations" ボタンを押します。</li>
 <li>パフォーマンスプロファイルの記録を始めます。</li>
 <li>"Do pointless computations!" ボタンを 2~3 回押します。</li>
 <li>プロファイルの記録を終了します。</li>
</ul>

<p>どのような結果になるかはマシンにより異なりますが、おそらく以下のようになるでしょう:</p>

<p><a id="js-blocking-overview" name="js-blocking-overview"><img alt="" src="https://mdn.mozillademos.org/files/10891/perf-js-blocking-overview.png" style="display: block; height: 113px; margin-left: auto; margin-right: auto; width: 800px;"></a></p>

<p>この画像の上半分は<a href="/ja/docs/Tools/Performance/UI_Tour#Waterfall_overview">ウォーターフォールの概要</a>です。これは<a href="/ja/docs/Tools/Performance/Waterfall">ウォーターフォール</a>をコンパクトに表示したビューであり、記録中にブラウザが行った処理は何かを示します。<a href="/ja/docs/Tools/Performance/Waterfall#Markers">桃色はほとんどの場合 CSS の再計算、一部はリフローです</a>。これは、プロファイルで終始実行している CSS アニメーションです。また連続したの橙色のブロックが 3 つありますが、これは JavaScript を実行していることを表します。それぞれ、ボタンを押したときです。</p>

<p>下半分はタイムラインの概要と時系列が合わせられており、<a href="/ja/docs/Tools/Performance/Frame_rate">フレームレート</a>を示しています。記録中のほとんどはフレームレートが良好ですが、ボタンを押すたびに大きく落ち込んでいます。</p>

<p>それら 3 か所のうちひとつを選択して、メインのウォーターフォールビューで詳しく見ることができます:</p>

<p><img alt="" src="https://mdn.mozillademos.org/files/10895/perf-js-blocking-waterfall.png" style="display: block; height: 306px; margin-left: auto; margin-right: auto; width: 800px;"></p>

<p>ここではボタンを押したときに、ブラウザが JavaScript の関数をひとつまたは連続的に実行して、メインスレッドを 71.73 ミリ秒、言い換えるとフレーム 4 つ分の時間ブロックしています。</p>

<p>どの関数でしょう? <a href="/ja/docs/Tools/Performance/Flame_Chart">フレームチャート</a>ビューに切り替えると、それがわかります:</p>

<p><img alt="" src="https://mdn.mozillademos.org/files/10893/perf-js-blocking-flame-chart.png" style="display: block; height: 230px; margin-left: auto; margin-right: auto; width: 800px;"></p>

<p>これは、その時点で実行している JS のコールスタックを表示します。スタックの一番上は <code>calculatePrimes()</code> という関数であり、ファイル名や行番号がわかります。以下に掲載したコードで、直近の呼び出し元を見てみましょう:</p>

<pre class="brush: js">const iterations = 50;
const multiplier = 1000000000;

function calculatePrimes(iterations, multiplier) {
  var primes = [];
  for (var i = 0; i &lt; iterations; i++) {
    var candidate = i * (multiplier * Math.random());
    var isPrime = true;
    for (var c = 2; c &lt;= Math.sqrt(candidate); ++c) {
      if (candidate % c === 0) {
          // not prime
          isPrime = false;
          break;
       }
    }
    if (isPrime) {
      primes.push(candidate);
    }
  }
  return primes;
}

function doPointlessComputationsWithBlocking() {
  var primes = calculatePrimes(iterations, multiplier);
  pointlessComputationsButton.disabled = false;
  console.log(primes);
}
</pre>

<p>ここではかなり大きな数に対して、(とても非効率な) 素数の判定を 50 回行っています。</p>

<h2 id="Using_requestAnimationFrame" name="Using_requestAnimationFrame">requestAnimationFrame を使用する</h2>

<p>この問題を解決するための最初の試みとして、関数をいくつかの自己充足した小さな関数に分割して、<code><a href="/ja/docs/Web/API/window/requestAnimationFrame">requestAnimationFrame()</a></code> を使用してそれらをスケジューリングします。</p>

<p><code>requestAnimationFrame()</code> は与えられた関数を、各フレームで再描画を行う直前に実行するようブラウザに指示します。それぞれの関数が適度に小さければ、ブラウザは実行時間を、フレーム間に与えられた時間内に収めることができるでしょう。</p>

<p><code>calculatePrimes()</code> の分割はとてもシンプルです。別の関数で、それぞれの値が素数であるかの計算を行います:</p>

<pre class="brush: js">function doPointlessComputationsWithRequestAnimationFrame() {

  function testCandidate(index) {
    // finishing condition
    if (index == iterations) {
      console.log(primes);
      pointlessComputationsButton.disabled = false;
      return;
    }
    // test this number
    var candidate = index * (multiplier * Math.random());
    var isPrime = true;
    for (var c = 2; c &lt;= Math.sqrt(candidate); ++c) {
      if (candidate % c === 0) {
          // not prime
          isPrime = false;
          break;
       }
    }
    if (isPrime) {
      primes.push(candidate);
    }
    // schedule the next
    var testFunction = testCandidate.bind(this, index + 1);
    window.requestAnimationFrame(testFunction);
  }

  var primes = [];
  var testFunction = testCandidate.bind(this, 0);
  window.requestAnimationFrame(testFunction);
}</pre>

<p>こちらのバージョンを試してみましょう。"Use requestAnimationFrame" と記載されたラジオボタンを選択して、新たにプロファイルを記録します。すると、記録は以下のようになるでしょう:</p>

<p><a id="js-raf-overview" name="js-raf-overview"><img alt="" src="https://mdn.mozillademos.org/files/10897/perf-js-raf-overview.png" style="display: block; height: 112px; margin-left: auto; margin-right: auto; width: 800px;"></a></p>

<p>これはまさに、私たちが期待していたものです。<a href="#js-blocking-overview">一続きの橙色のブロック</a>に代わり、ボタンを押すたびにとても短い橙色のブロックがたくさん並んでいます。橙色のブロックは 1 個ずつのフレームに分かれて現れており、またそれぞれのブロックが、<code>requestAnimationFrame()</code> から呼び出された関数 1 個を表しています。なお、このプロファイルではボタンを 2 回しか押していないことに注意してください。</p>

<p>関数の呼び出しは CSS アニメーションに由来する桃色のブロックの間に挟み込まれており、またそれぞれの関数は、全体のフレームレートを落とすことなく処理できるほど十分に小さくなっています。</p>

<p>ここでは <code>requestAnimationFrame</code> が応答性の問題の解決策として機能しましたが、潜在的な問題点が 2 つあります:</p>

<ul>
 <li>長い間実行する関数を、個別の自己充足した関数に分割することが難しい場合があります。今回のシンプルなケースでも、より複雑なコードになりました。</li>
 <li>分割したバージョンでは、実行時間が長くなります。実は、処理にどれだけかかるかをかなり正確に言うことができます。処理は 50 回繰り返しており、またブラウザは 1 秒間に約 60 個のフレームを生成します。よって、すべての計算を実行するためにはほぼ 1 秒かかり、これはユーザエクスペリエンスやプロファイルから明らかになります。</li>
</ul>

<h2 id="Using_web_workers" name="Using_web_workers">Web Worker を使用する</h2>

<p>ここでは、Web Worker を使用して問題を解決します。Web Worker を使用すると、別のスレッドで JavaScript を実行できます。メインスレッドと Worker スレッドは互いに直接呼び出すことはできませんが、非同期メッセージ API を使用して通信できます。</p>

<p>メインスレッドのコードは以下のようになります:</p>

<pre class="brush: js">const iterations = 50;
const multiplier = 1000000000;

var worker = new Worker("js/calculate.js");

function doPointlessComputationsInWorker() {

  function handleWorkerCompletion(message) {
    if (message.data.command == "done") {
      pointlessComputationsButton.disabled = false;
      console.log(message.data.primes);
      worker.removeEventListener("message", handleWorkerCompletion);
    }
  }

  worker.addEventListener("message", handleWorkerCompletion, false);

  worker.postMessage({
    "multiplier": multiplier,
    "iterations": iterations
  });
}</pre>

<p>元のコードと比べたときの主な違いは、以下のものが必要であることです:</p>

<ul>
 <li>Worker を作成する</li>
 <li>計算の準備ができたときに、Worker へメッセージを送信する</li>
 <li>"done" メッセージをリッスンする。これは、Worker が完了したことを示すメッセージです。</li>
</ul>

<p>また、新たに "calculate.js" ファイルが必要であり、こちらは以下のようになります:</p>

<pre class="brush: js">self.addEventListener("message", go);

function go(message) {
  var iterations = message.data.iterations;
  var multiplier = message.data.multiplier;
  primes = calculatePrimes(iterations, multiplier);

  self.postMessage({
    "command":"done",
    "primes": primes
  });
}

function calculatePrimes(iterations, multiplier) {
  var primes = [];
  for (var i = 0; i &lt; iterations; i++) {
    var candidate = i * (multiplier * Math.random());
    var isPrime = true;
    for (var c = 2; c &lt;= Math.sqrt(candidate); ++c) {
      if (candidate % c === 0) {
          // not prime
          isPrime = false;
          break;
       }
    }
    if (isPrime) {
      primes.push(candidate);
    }
  }
  return primes;
}</pre>

<p>Worker では処理の開始を指示するメッセージをリッスンする、および処理が完了したときに "done" メッセージを送ることが必要です。実際に計算を行っている部分のコードは、最初のバージョンのコードと完全に同じです。</p>

<p>このバージョンはどのように実行されるのでしょう? ラジオボタンを "Use a worker" に切り替えて、新たにプロファイルを記録してください。結果は以下のようになるでしょう:</p>

<p><img alt="" src="https://mdn.mozillademos.org/files/10899/perf-js-worker-overview.png" style="display: block; height: 112px; margin-left: auto; margin-right: auto; width: 800px;"></p>

<p>このプロファイルでは、ボタンを 3 回押しています。ウォーターフォールの概要で<a href="#js-blocking-overview">元のバージョンと比べると</a>、ボタンを押したときにはとても短い橙色のマーカーが 2 個あることがわかります:</p>

<ul>
 <li>click イベントの処理と Worker の処理開始を行う、<code>doPointlessComputationsInWorker()</code> 関数</li>
 <li>Worker が "done" を発信したときに実行される、<code>handleWorkerCompletion()</code> 関数</li>
</ul>

<p>これら 2 つの関数の間で Worker は素数の判定を行っていますが、メインスレッドの応答性には少しも影響を与えていないように見受けられます。これはあり得ないと思うかもしれませんが、Worker は別のスレッドで実行しますのでマルチコアプロセッサの利点を享受できます。これはシングルスレッドの Web サイトでは得られません。</p>

<p>Web Worker の主な制限は、Worker で実行するコードでは DOM API を使用できないことです。</p>