コピペで完結!タブ切替 #09【初期表示を速くするためのタブ】
html/css/js
2025.10.05
初期表示を速くするためのタブ(Lazy Loading)
ページを開いた直後から全タブの中身を描画すると、表示が重くなり読込時間も延びてしまいます。
そこで最初は「概要」だけを表示し、他のタブはクリックされた時点で初回のみ読み込む仕組みにすることで、初期表示を大幅に軽くできます。
- 必要なタイミングだけロードすることで初期表示の高速化
- 以降はキャッシュを使い、再読み込みの無駄を削減
- 読み込み中はスピナーを表示し、ユーザー体験を維持
<template>
やdata-*
属性を利用し、シンプルに実装可能
コードについて
本記事のコードは AI(ChatGPT)による生成をベースに作成・調整しています。ご利用の環境でテストの上ご使用ください。
免責
本コードの利用に伴う不具合・損害について、当サイトは責任を負いません。自己責任にてご利用ください。
デモ
最初に表示されるのは「概要」だけ。他のタブは、クリックされた時点でテンプレートから読み込みます(初回のみ)。
タブ内コンテンツを、初回クリック時に遅延ロードします。読み込み中はスピナーを表示し、完了後は結果をキャッシュします。
遅延挿入された画像1
遅延挿入された画像2
遅延挿入された画像3
大きめのリスト(初回のみ挿入)
- リスト項目A — 遅延描画で初期パフォーマンスを確保
- リスト項目B — クリック時のみDOMに挿入
- リスト項目C — 以後はキャッシュされた内容を再利用
- リスト項目D — スクロール時のレイアウトシフトを抑制
- リスト項目E — ネットワーク転送量の削減
外部HTML(疑似)
実運用では fetch()
で外部HTMLを取得し、初回のみ挿入します。このデモではネットワーク代替として
<template>
からの挿入でふるまいを再現しています。
コードをコピーして使おう!
/* =========================================
タブ内コンテンツ遅延読み込み(Lazy):CSS
-----------------------------------------
・必要最小限のスタイルで、他スタイルと衝突しないよう
セレクタは #demo-section 配下に限定しています。
========================================= */
/* ルート領域(タイポと色のトークン定義) */
#demo-section.demo-wrap{
max-width: 960px; /* 横幅上限(レイアウトの基準) */
margin: 0 auto; /* コンテンツ中央寄せ */
padding: 0 16px; /* 左右の余白 */
font-family: system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans JP","Yu Gothic UI",sans-serif; /* 読みやすいフォント */
color: #0f172a; /* 基本文字色 */
--ink:#0f172a; /* 文字色トークン */
--muted:#64748b; /* 補助文字色トークン */
--accent:#0b6bff; /* 強調色トークン */
--border:#e5e7eb; /* 枠線色トークン */
--panel:#ffffff; /* パネル背景トークン */
}
/* タブバー(ボタンを横並び、折返し可) */
#demo-section .tabs{
display:flex; /* 横並び */
flex-wrap:wrap; /* 画面が狭い時は折返し */
gap:8px; /* ボタン間の余白 */
padding:8px; /* タブバー内側余白 */
background:#f8fafc; /* 下地背景 */
border:1px solid var(--border); /* 外枠線 */
border-radius:12px; /* 角丸 */
margin: 0 0 12px; /* 下のパネルとの間隔 */
}
/* タブボタンの見た目 */
#demo-section .tab{
appearance:none; /* OS既定の装飾を解除 */
border:1px solid var(--border); /* 薄い枠線 */
background:#fff; /* 背景 */
color:var(--ink); /* 文字色 */
padding:8px 14px; /* クリックしやすい余白 */
border-radius:10px; /* 角丸 */
font-size:14px; /* 文字サイズ */
cursor:pointer; /* ホバー時のカーソル */
transition:background .2s,color .2s,border-color .2s; /* 色変化を滑らかに */
}
/* 選択中タブ(JSが aria-selected=true を付与) */
#demo-section .tab[aria-selected="true"]{
background:var(--accent); /* 背景を強調色に */
border-color:var(--accent); /* 枠線も強調色 */
color:#fff; /* 文字は白 */
font-weight:600; /* わずかに太く */
}
/* パネル(コンテンツボックス) */
#demo-section .panel{
border:1px solid var(--border); /* 枠線 */
border-radius:12px; /* 角丸 */
background:var(--panel); /* 背景色 */
padding:16px; /* 余白 */
min-height:160px; /* ローディング時の最小高さ確保 */
}
/* ローディング表示(ドット3つの簡易アニメーション) */
#demo-section .loading{
display:flex; gap:8px; align-items:center; justify-content:center;
height:120px; color:#475569; font-size:13px;
}
#demo-section .loading .dot{
width:8px; height:8px; border-radius:999px; background:#94a3b8;
animation: dBounce 1.2s infinite ease-in-out;
}
#demo-section .loading .dot:nth-child(2){ animation-delay:.15s; }
#demo-section .loading .dot:nth-child(3){ animation-delay:.3s; }
@keyframes dBounce{
0%, 80%, 100% { transform: scale(0); opacity:.5; }
40% { transform: scale(1); opacity:1; }
}
<!-- =========================================
タブ内コンテンツ遅延読み込み(Lazy):HTML
-----------------------------------------
・role="tablist" / role="tab" / role="tabpanel" でアクセシブルに
・各タブに data-lazy-* 属性で「何をどう挿入するか」を宣言
- data-lazy="none" … 遅延なし(初期表示パネルなど)
- data-lazy-template="#id" … <template> 要素から挿入
- data-lazy-html="#id" … 外部HTML想定(デモでは <template> を代替)
・初回クリック時に内容を注入し、以後は data-loaded="true" を見て再注入しない
========================================= -->
<section id="demo-section" class="demo-wrap" aria-label="デモ">
<!-- タブバー -->
<div class="tabs" role="tablist" aria-label="Lazyタブ">
<button class="tab" role="tab" id="tab-1" aria-controls="panel-1" aria-selected="true"
data-lazy="none">概要</button>
<button class="tab" role="tab" id="tab-2" aria-controls="panel-2" aria-selected="false"
data-lazy-template="#tmpl-images">画像(テンプレート)</button>
<button class="tab" role="tab" id="tab-3" aria-controls="panel-3" aria-selected="false"
data-lazy-template="#tmpl-list">長文リスト(テンプレート)</button>
<button class="tab" role="tab" id="tab-4" aria-controls="panel-4" aria-selected="false"
data-lazy-html="#tmpl-remote">外部HTML想定(疑似)</button>
</div>
<!-- パネル(#1 は即内容、それ以外は空で hidden) -->
<div id="panel-1" class="panel" role="tabpanel" aria-labelledby="tab-1">
初期表示ではこのパネルのみ描画し、他タブはクリック時に初回ロードします。
</div>
<div id="panel-2" class="panel" role="tabpanel" aria-labelledby="tab-2" hidden></div>
<div id="panel-3" class="panel" role="tabpanel" aria-labelledby="tab-3" hidden></div>
<div id="panel-4" class="panel" role="tabpanel" aria-labelledby="tab-4" hidden></div>
<!-- Lazy 用テンプレート(実運用では fetch で外部HTMLを取得してもOK) -->
<template id="tmpl-images">
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;">
<figure style="margin:0">
<img loading="lazy" src="https://picsum.photos/320/180?random=21" alt="サンプル画像1"
style="width:100%;height:auto;border-radius:10px;border:1px solid #e5e7eb;">
<figcaption style="font-size:12px;color:#64748b;margin-top:6px;">遅延挿入された画像1</figcaption>
</figure>
<figure style="margin:0">
<img loading="lazy" src="https://picsum.photos/320/180?random=22" alt="サンプル画像2"
style="width:100%;height:auto;border-radius:10px;border:1px solid #e5e7eb;">
<figcaption style="font-size:12px;color:#64748b;margin-top:6px;">遅延挿入された画像2</figcaption>
</figure>
<figure style="margin:0">
<img loading="lazy" src="https://picsum.photos/320/180?random=23" alt="サンプル画像3"
style="width:100%;height:auto;border-radius:10px;border:1px solid #e5e7eb;">
<figcaption style="font-size:12px;color:#64748b;margin-top:6px;">遅延挿入された画像3</figcaption>
</figure>
</div>
</template>
<template id="tmpl-list">
<div>
<h3 style="margin:0 0 8px;font-size:16px;">大きめのリスト(初回のみ挿入)</h3>
<ol style="margin:0;padding-left:1.2em;line-height:1.85;color:#334155;">
<li>リスト項目A — 遅延描画で初期パフォーマンスを確保</li>
<li>リスト項目B — クリック時のみDOMに挿入</li>
<li>リスト項目C — 以後はキャッシュされた内容を再利用</li>
<li>リスト項目D — スクロール時のレイアウトシフトを抑制</li>
<li>リスト項目E — ネットワーク転送量の削減</li>
</ol>
</div>
</template>
<!-- 外部HTML想定(デモでは template を代替。実運用は fetch で置換) -->
<template id="tmpl-remote">
<article>
<h3 style="margin:0 0 8px;font-size:16px;">外部HTML(疑似)</h3>
<p style="margin:0;color:#334155;line-height:1.85;">
実運用では <code>fetch()</code> で外部HTMLを取得し、初回のみ挿入します。
このデモでは <code><template></code> からの挿入で挙動を再現しています。
</p>
</article>
</template>
</section>
(function(){
/* =========================================
タブ内コンテンツ遅延読み込み(Lazy):JS
-----------------------------------------
役割:
- クリックされたタブに応じてパネルの表示を切替
- 初回だけテンプレート/外部HTMLを注入(以後はキャッシュ)
- 読み込み中は3ドットの簡易ローディングを表示
========================================= */
/* -----------------------------------------
デモ領域のルート要素を取得
id="demo-section" が無ければ処理を中断
----------------------------------------- */
const root = document.getElementById('demo-section');
if(!root) return;
/* -----------------------------------------
タブとパネルのNodeListを取得
- tabs : role="tab" を持つボタン群
- panels : role="tabpanel" を持つコンテンツ群
----------------------------------------- */
const tabs = root.querySelectorAll('.tab[role="tab"]');
const panels = root.querySelectorAll('.panel[role="tabpanel"]');
/* -----------------------------------------
3ドットのローディングUIをパネルに表示
- パネル内容をクリアして .loading コンテナを挿入
- CSSで .loading .dot にアニメーションを付与
----------------------------------------- */
function showLoading(panel){
panel.innerHTML = ''; // 既存の内容を消去
const wrap = document.createElement('div'); // ラッパー生成
wrap.className = 'loading'; // CSSアニメ用クラス
// ドット3つを挿入(CSSで点滅アニメ)
wrap.innerHTML = '';
panel.appendChild(wrap); // パネルに追加
}
/* -----------------------------------------
template要素を複製してパネルに挿入
- data-lazy-template="#id" に対応
----------------------------------------- */
function injectTemplate(panel, selector){
const tmpl = root.querySelector(selector); // 例: "#tmpl-images"
if(!tmpl){
panel.innerHTML = '(template が見つかりません)';
return;
}
panel.innerHTML = ''; // ローディングを消去
panel.appendChild(tmpl.content.cloneNode(true)); // 内容を複製して追加
}
/* -----------------------------------------
疑似:外部HTML挿入
- 実運用では fetch() で取得したHTMLを挿入
- デモでは template を代用
----------------------------------------- */
function injectRemote(panel, selector){
/* 実際の処理例:
fetch('/path/fragment.html')
.then(r => r.text())
.then(html => { panel.innerHTML = html; });
*/
injectTemplate(panel, selector); // デモではtemplateを代用
}
/* -----------------------------------------
初回ロードだけ実行
- data-loaded 属性をフラグとして使用
- 遅延対象ならローディングを表示し、コンテンツを挿入
----------------------------------------- */
function ensureLoaded(tab, panel){
if(panel.dataset.loaded === 'true') return; // 既にロード済みなら何もしない
const mode = tab.getAttribute('data-lazy'); // "none" または null
const tSel = tab.getAttribute('data-lazy-template'); // 例: "#tmpl-images"
const hSel = tab.getAttribute('data-lazy-html'); // 例: "#tmpl-remote"
if(mode === 'none'){ // 遅延不要(初期パネル等)
panel.dataset.loaded = 'true';
return;
}
showLoading(panel); // ローディング表示
// 疑似的に遅延 (400ms)。実運用のfetchでは不要
setTimeout(() => {
if(tSel) injectTemplate(panel, tSel);
else if(hSel) injectRemote(panel, hSel);
else panel.innerHTML = '(読み込むテンプレート/HTMLが指定されていません)';
panel.dataset.loaded = 'true'; // 次回以降は読み込みスキップ
}, 400);
}
/* -----------------------------------------
タブ切替処理
- aria-selected を更新
- 表示するパネル以外を hidden に
- ensureLoaded() で初回ロード保証
----------------------------------------- */
function activate(tab){
// タブの選択状態を更新
tabs.forEach(t => t.setAttribute('aria-selected', String(t === tab)));
// パネルの表示/非表示を切替
panels.forEach(p => {
const show = p.id === tab.getAttribute('aria-controls');
p.hidden = !show;
});
// 選択されたパネルを取得
const panel = root.querySelector('#' + tab.getAttribute('aria-controls'));
ensureLoaded(tab, panel); // 初回ロードを保証
}
/* -----------------------------------------
イベント登録
各タブにクリックイベントを付与
----------------------------------------- */
tabs.forEach(tab => tab.addEventListener('click', () => activate(tab)));
/* -----------------------------------------
初期表示
- aria-selected="true" のタブを探す
- 無ければ先頭タブを初期アクティブにする
----------------------------------------- */
const first = root.querySelector('.tab[aria-selected="true"]') || tabs[0];
if(first) activate(first);
})();
コピペで完結!タブ切替 #09【初期表示を速くするためのタブ】
コメント