JavaScriptでinnerHTMLとappendChildの速度比較
最近の検索エンジンはJavaScriptで表示されたWebページもほとんど正確に読み取ることが出来るそうで、動的にページを書き換えるサイトも増えてきました。
特にページ切り替えの動作はURLの後ろに p=1, p=2, p=3 等とページ番号を付けてページ全体を更新する方法が当たり前でしたが、いちいちページ全体を読み込むのも無駄が多いため、HTMLの要素内の、例えばTABLEタグやDLタグを再生成してページ切り替えをすることも増えました。
そこで、要素の再生成ですが、innerHTMLで書き換えるのが早いのか、それともappendChildで書き換えるのが良いのか、それとも噂に聞くinsertAdjacentHTMLが最適なのか、軽く速度テストをしてみました。
HTMLを準備
パフォーマンステスト用にこんなHTMLを用意しました。
<html>
<body>
<input type="text" id="test_count" value="1000" placeholder="テスト回数">
<button onclick="testAll();">テスト開始</button>
<div id="test" style="height:100px;overflow:scroll;"></div>
<div id="result"></div>
<script>
function testAll()
{
new Promise (test1);
new Promise (test2);
new Promise (test1a);
new Promise (test2a);
new Promise (test3);
}
</script>
</body>
</html>
全部で5パターンのテストをしたいのでtestAllの中からそれぞれの関数を呼んでいます。
その都度innerHTMLに要素を追加するテスト
テストコード
function test1() {
document.getElementById('test').textContent = '';
let test_count = document.getElementById('test_count').value;
t1 = performance.now();
//
let div = document.getElementById('test');
div.innerHTML = '<dl>';
for (let i = 1; i <= test_count; i++) {
div.innerHTML += '<dt>'+i+'</dt><dd>A</dd><dd>B</dd><dd>C</dd>';
}
div.innerHTML += '</dl>';
//
t2 = performance.now();
document.getElementById('result').innerHTML += 'その都度innerHTML:'+(t2 - t1)+'ミリ秒<br>';
}
div要素を取得し、そのinnerHTMLに毎回+=で要素を追加していくという、いかにも遅そうな例です。
結果
うちの環境(Ryzen5 3600)では、1000行追加するのに4,224ミリ秒(約4.2秒)もかかりました。
innerHTMLは書き換えが発生するたびにDOM要素が再解析されるので、+=で追加していくやり方はよくなさそうです。
その都度insertAdjacentHTMLで要素を追加するテスト
テストコード
function test2() {
document.getElementById('test').textContent = '';
let test_count = document.getElementById('test_count').value;
t1 = performance.now();
//
let div = document.getElementById('test');
div.insertAdjacentHTML('beforeend', '<dl>');
for (let i = 1; i <= test_count; i++) {
div.insertAdjacentHTML('beforeend', '<dt>'+i+'</dt><dd>A</dd><dd>B</dd><dd>C</dd>');
}
div.insertAdjacentHTML('beforeend', '</dl>');
//
t2 = performance.now();
document.getElementById('result').innerHTML += 'その都度insertAdjacentHTML:'+(t2 - t1)+'ミリ秒<br>';
}
前回のinnerHTMLに+=で追加する部分をinsertAdjacentHTML関数に書き換えただけです。このほうが早いとは聞くものの、実際に使うのははじめてです。
結果
なんと1,000行の追加に9.6ミリ秒でした。前回の4,224ミリ秒に比べて440倍も早くなりましたw
この後実施するメモリ中にHTMLを作ってまとめてセットする方法に比べると、その都度insertAdjacentHTML関数を呼び出しているので遅いだろうと考えていたのですが、思ったより早くて驚きました。
まとめてinnerHTMLに要素をセットするテスト
テストコード
function test1a() {
document.getElementById('test').textContent = '';
let test_count = document.getElementById('test_count').value;
t1 = performance.now();
//
let html = '<dl>';
for (let i = 1; i <= test_count; i++) {
html += '<dt>'+i+'</dt><dd>A</dd><dd>B</dd><dd>C</dd>';
}
html += '</dl>';
document.getElementById('test').innerHTML = html;
//
t2 = performance.now();
document.getElementById('result').innerHTML += 'まとめてinnerHTML:'+(t2 - t1)+'ミリ秒<br>';
}
これはぼくが実際によく使う例です。htmlという変数を用意し、そこにHTMLタグをその都度追加し、最後にhtml変数をinnerHTMLへ上書きしています。これで遅いと感じたことはないので、そこそこの秒数が出るのではないかなと予想。
結果
1,000行の追加にわずか5.5ミリ秒でした。その都度innerHTMLに追加した4,224ミリ秒に比べて768倍。やはり、メモリ中にHTMLタグを書いて、一度でinnerHTMLへセットする方法は悪くなさそうです。
まとめてinsertAdjacentHTMLで要素をセットするテスト
テストコード
function test2a() {
document.getElementById('test').textContent = '';
let test_count = document.getElementById('test_count').value;
t1 = performance.now();
//
let html = '<dl>';
for (let i = 1; i <= test_count; i++) {
html += '<dt>'+i+'</dt><dd>A</dd><dd>B</dd><dd>C</dd>';
}
html += '</dl>';
document.getElementById('test').insertAdjacentHTML('beforeend', html);
//
t2 = performance.now();
document.getElementById('result').innerHTML += 'まとめてinsertAdjacentHTML:'+(t2 - t1)+'ミリ秒<br>';
}
先程のまとめてinnerHTMLへセットする部分をinsertAdjacentHTMLに書き換えただけです。恐らくたいして変わらないとは思いますが、念の為やってみました。
結果
1000行の追加に同じく5.5ミリ秒でした。0.1ミリ秒のズレもありません。メモリにHTMLタグを書いてまとめてセットする方法ではinnerHTMLとinsertAdjacentHTMLに差はなさそうです。
appendChildを使って要素を追加する例
テストコード
function test3() {
document.getElementById('test').textContent = '';
let test_count = document.getElementById('test_count').value;
t1 = performance.now();
//
let div = document.getElementById('test');
let dl = document.createElement('dl');
for (let i = 1; i <= test_count; i++) {
let dt = document.createElement('dt');
dt.textContent = i;
dl.appendChild(dt);
let dd = document.createElement('dd');
dd.textContent = 'A';
dl.appendChild(dd);
dd = document.createElement('dd');
dd.textContent = 'B';
dl.appendChild(dd);
dd = document.createElement('dd');
dd.textContent = 'C';
dl.appendChild(dd);
}
div.appendChild(dl);
//
t2 = performance.now();
document.getElementById('result').innerHTML += 'appendChild:'+(t2 - t1)+'ミリ秒<br>';
}
このテストをしようと思いついたときはinnerHTMLやinsertAdjacentHTMLを使うより、appendChildを使ったほうが無駄な解析がない分早くなるのではないか、そんな期待がありましたが、実際にコードを書き始めると、あれ?これけっこう無駄な書き方なのではないか…と不安に感じてきます。
特にddタグの追加部分なんて非常にまわりくどくコード自体も長くなります。
結果
1,000行の追加に約6.4ミリ秒でした。
最初の4,224ミリ秒に比べればそりゃあものすごく早いですが、メモリにまとめてからinnerHTMLへセットする方法よりは遅くなってしまいました。
でも冷静に考えてみればそりゃそうか。JavaScript自体の速度がアレですから、タグの解析をブラウザにまかせてしまったほうが早いというのは納得のいく話です。
実際にテストを実行したい場合
下記の[テスト開始]ボタンを押すと同じテストが出来ます。
環境によってはすごく時間がかかるかもなので、デフォルトのテスト回数は100回にセットしています。
まとめ
- innerHTMLへその都度要素を追加すると再解析が発生するため、とっても遅い!
- どうしてもその都度追加したいならinsertAdjacentHTMLのほうが440倍早い
- その都度要素を追加する、という観点で見るとappendChildが最速
- まとめて書き換えて良いのなら、メモリ中にHTMLタグを作って上書きするのが一番早い
といったところでしょうか。
appendChildで少しがっかりもしましたが、よく考えたらその都度追加しているinsertAdjacentHTMLに比べて1.5倍早いですし、実際の使用においては細かく要素を使いたい例も多いと思います。
つまり、細かく要素を追加する場合→appendChild
まとめてページ切り替え等で使う場合→メモリにまとめてからinnerHTML
が良いのではないかなと感じました。