─ JavaScript で thead/tbody を動的に生成する際の落とし穴と対策
Webアプリで株価データをテーブル表示する機能を作っていたとき、不可解なバグに遭遇しました。
❗発生した問題
テーブル描画後、行の並びが次のように“おかしい”状態に:
1行目:株価データ(本来はここにヘッダーが来るべき) 2行目:ヘッダー行 3行目:株価データ(本来はヘッダーの直下に来るべき)
データとヘッダーが入れ替わってしまっているわけです。
HTML側の構造は正しいし、JS の描画処理も明らかな誤りはない。
にもかかわらず、ヘッダーがデータの間に割り込むという不可解な現象。
この記事では、
「この現象がなぜ起こるのか」
「どう直すか」
を体系的にまとめます。
🎯 結論(最初に答え)
この問題の根本原因は:
❗ table 内に残った「空白テキストノード」が DOM の先頭行扱いになり、thead/tbody が後ろに回されてしまうため
そしてそれを引き起こす要因は2つ:
- HTML に元からある
<thead>と<tbody>を JS が再生成している - table の削除処理で空白ノードが残り、ブラウザの自動補完で “tbody の中身” として扱われる
結果として、描画順序が
空白 → thead → tbody
の順に並び、ヘッダーが中間に来てしまいます。
🧠 何が起きていたのか?処理フローで徹底解説
以下、ブラウザと JavaScript の動きを時系列で追っていきます。
① HTML 初期状態
HTML はこんな感じでした:
<table id="analysis-table"> <thead></thead> <tbody></tbody> </table>
一見問題なさそうですが…
② ブラウザの table 自動補完が発動
ブラウザは table をパースするときに以下を行います:
- table 内の改行や空白を #text ノードとして扱う
- 不足している tbody を 自動生成 する
- テキストノードを tbody に押し込むことがある
すると DOM はこうなる可能性があります:
<table> #text ← table直下にある空白 <thead></thead> <tbody>#text(空白)</tbody> </table>
この「空白ノード (#text)」が厄介者です。
③ JS の描画処理が thead/tbody を削除
if (table.tHead) table.tHead.remove(); while (table.tBodies.length > 0) table.tBodies[0].remove();
この結果:
<table> #text ← これだけ残る </table>
空白ノードは削除されないため、そのまま table の先頭行扱いとなる。
④ JS が新しい thead / tbody を作成
table.createTHead(); table.createTBody();
ブラウザは残った #text の後ろに thead と tbody を追加します:
<table> #text ← 1行目扱い <thead>…</thead> ← 2行目 <tbody>…</tbody> ← 3行目 </table>
ここで 表示順序が狂う現象が確定 します。
⑤ tbody にデータ行を追加すると…
結果:
1行目:#text(ブラウザが「データっぽい1行目」と扱う)
2行目:ヘッダー
3行目:データ行(本来2行目になるべき)
🧩 なぜ解決が難しいのか?
この問題は通常のデバッグでは気づけません。
- 「空白ノード」は目に見えない
- table はブラウザが自動補完するため、HTML と DOM が完全に一致しない
- thead/tbody の生成・削除が DOM の順序を狂わせる
ブラウザ依存の隠れた仕様が絡むため、意図しない順序の入れ替わりが起こるわけです。
🛠️ 解決策(最も確実)
✅ 対策1:HTML から thead / tbody を削除してゼロから描画する(推奨)
<table id="analysis-table"></table>
JavaScript 側で完全に生成するパターンがもっとも安全です。
✅ 対策2:renderTable() の先頭で table.innerHTML を丸ごとクリアする
function renderTable() {
table.innerHTML = ""; // ←ここが重要
const thead = table.createTHead();
const tbody = table.createTBody();
...
}
空白ノードを含む すべての DOM を確実に消せる ので安全。
📝 教訓:table 要素は「ブラウザの自動補完」と相性が悪い
今回のバグから得られた教訓は次の通り:
1. table に初期の thead/tbody を書かないほうが安全
動的生成する場合は table の中身は空 にしておくのが鉄則。
2. innerHTML = "" は意外と重要
削除系 API(remove(), removeChild())では 空白ノードが残る。
3. ブラウザは table を「勝手に修正」する
HTML と実際の DOM は必ずしも一致しません。
🎉 まとめ
今回の問題の正体は:
JS が table 内の thead/tbody を削除した際に、ブラウザが補完した空白ノードが残り、
そのノードが “1行目” として扱われた結果、ヘッダーがデータの間に入り込んだ
という、ブラウザ仕様 × DOM 操作の複合的な副作用と言えそうです。
動的にテーブルを描画する場合は、
- HTML には空の table を置く
- innerHTML で完全クリアする
これが最も安全なベストプラクティスのようです。


