動的にスクリプトやスタイルを読み込む

以下では、DOMで動的にスクリプト(JavaScript)やスタイル(CSS)を読み込むための実践的方法を、仕組み・順序制御・エラーハンドリング・パフォーマンス・セキュリティまで含めて体系的に解説します。なお、説明文中では絵文字を使用しません。

1. 動的ロードの基礎

1.1 スクリプトの動的挿入(外部JS)

js
function loadScript(src, { module = false, // ES Modulesとして読み込む場合 true async = true, // 読み込み完了しだい実行(順序より速さ優先) defer = false, // パーサブロッキングを回避(parser-insertedに意味あり) attrs = {}, // integrity, crossorigin, referrerpolicy, nonce 等 timeoutMs = 15000 } = {}) { return new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = src; if (module) s.type = 'module'; // 動的に作った <script> はデフォルトで「非同期実行」に近い挙動 // 複数の順序を保証したい場合は async=false を指定(下記 2.1 参照) s.async = !!async; if (defer) s.defer = true; // 動的挿入では多くのブラウザで効果が限定的 // 追加属性(SRI, CORS, CSP など) for (const [k, v] of Object.entries(attrs)) s.setAttribute(k, v); const timer = setTimeout(() => { s.remove(); reject(new Error(`Script load timeout: ${src}`)); }, timeoutMs); s.onload = () => { clearTimeout(timer); resolve(s); }; s.onerror = () => { clearTimeout(timer); s.remove(); reject(new Error(`Script load error: ${src}`)); }; document.head.appendChild(s); }); }

重要ポイント

  • innerHTML<script>を挿入しても、多くのブラウザで実行されません。必ずcreateElement('script')してDOMに挿入します。

  • 動的に追加した<script>は、読み込み完了順で実行されがちです(順序が崩れる)。順序固定は後述。

1.2 スタイルの動的挿入(外部CSS)

js
function loadCSS(href, { media = 'all', attrs = {}, // integrity, crossorigin, referrerpolicy, title など timeoutMs = 15000 } = {}) { return new Promise((resolve, reject) => { const l = document.createElement('link'); l.rel = 'stylesheet'; l.href = href; l.media = media; for (const [k, v] of Object.entries(attrs)) l.setAttribute(k, v); const timer = setTimeout(() => { l.remove(); reject(new Error(`CSS load timeout: ${href}`)); }, timeoutMs); l.onload = () => { clearTimeout(timer); resolve(l); }; l.onerror = () => { clearTimeout(timer); l.remove(); reject(new Error(`CSS load error: ${href}`)); }; document.head.appendChild(l); }); }

1.3 インラインスタイルの動的生成

js
function injectStyle(cssText, { id, nonce } = {}) { const style = document.createElement('style'); if (id) style.id = id; if (nonce) style.nonce = nonce; // CSP利用時 style.textContent = cssText; document.head.appendChild(style); return style; }

2. 実行順序と依存関係

2.1 依存関係のある複数スクリプト

動的挿入した<script>は基本的に並行取得→到着順に実行されます。依存関係がある場合は、順次ロードにします。

js
async function loadScriptsInOrder(urls, options) { // 順序を守りたいので async=false を強制 for (const url of urls) { await loadScript(url, { ...options, async: false }); } }
  • deferはパーサ挿入時(HTML内に直書き)に効果的ですが、動的挿入では効果が限定的です。

  • ES Modules を使うなら、**動的import()**も有効です(後述)。

2.2 ES Modules の動的読み込み(推奨)

依存解決をブラウザに任せられるため、モジュール時代は**import()**が便利です。

js
// 条件に応じて必要な時だけ読み込む async function ensureFeature() { const { initFeature } = await import('/js/feature.js'); initFeature(); }
  • import()Promiseを返すため、非同期制御が容易です。

  • 事前に<link rel="modulepreload" href="/js/feature.js">を置くと依存モジュールも先行取得でき、体感が改善する場合があります。

3. パフォーマンス最適化

  • 遅延ロード: 使う直前まで読み込まない。IntersectionObserverで要素が見えたら読み込み、requestIdleCallbackでアイドル時間に読み込みなど。

  • プリロード:

    • スクリプト: <link rel="preload" as="script" href="...">

    • CSS: <link rel="preload" as="style" href="...">onloadrel="stylesheet"に切り替える手法もある(FOUC対策を要検討)。

  • プリコネクト/ DNSプリフェッチ:
    <link rel="preconnect" href="https://cdn.example.com">
    <link rel="dns-prefetch" href="//cdn.example.com">

  • キャッシュ制御: バージョン付きクエリ(?v=123)でキャッシュバスティング。逆に長期キャッシュ可能なファイルはimmutable戦略。

  • 重複読み込みの防止: data-keyidで挿入済みかを判定。

js
function loadScriptOnce(src, key = src) { if (document.querySelector(`script[data-key="${CSS.escape(key)}"]`)) { return Promise.resolve('already-loaded'); } return loadScript(src, { attrs: { 'data-key': key } }); }

4. エラーハンドリングとフォールバック

  • onerrorで代替CDNへ切替:

js
async function loadWithFallback(primary, fallback) { try { await loadScript(primary); } catch { await loadScript(fallback); } }
  • タイムアウトを設ける(上記実装例参照)。

  • CSSの失敗時もonerrorで検知可能。必要なら代替テーマや簡易スタイルを適用。

5. セキュリティ(CSP / SRI / CORS)

  • CSP(Content Security Policy):

    • インライン<style><script>を許可するにはnonceまたはhashが必要。サーバが発行するnonceをDOM挿入時に付与します。

    • 外部リソースの読み込み先はscript-src/style-srcで許可ドメインに限定。

  • SRI(Subresource Integrity):

    • <script src="..." integrity="sha384-..." crossorigin="anonymous">

    • <link rel="stylesheet" href="..." integrity="sha384-..." crossorigin="anonymous">
      改ざん検知に有効。crossoriginとセットで使うのが一般的。

  • URLのサニタイズ:

    • ユーザー入力をそのままsrc/hrefに使わない。ホワイトリスト方式で許可。

  • @importは避ける: CSS内の@importは遅く、CSPの観点でも管理が複雑になりがち。

6. スタイル適用の高度テクニック

6.1 CSSOM(insertRule/removeRule)

既存の<style>要素のシートを直接編集できます。

js
const style = injectStyle(''); const sheet = style.sheet; // CSSStyleSheet sheet.insertRule('body { accent-color: rebeccapurple; }', sheet.cssRules.length); // sheet.deleteRule(index);

6.2 Constructable Stylesheets(adoptedStyleSheets)

パフォーマンスに優れ、Shadow DOMでも再利用しやすい手法です(未対応ブラウザがある点に注意)。

js
const sheet = new CSSStyleSheet(); sheet.replaceSync(` .badge { padding: 2px 6px; border-radius: 999px; font-weight: 600; } `); document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; // Shadow DOM内での適用例 const host = document.querySelector('#widget'); const root = host.attachShadow({ mode: 'open' }); root.adoptedStyleSheets = [sheet]; root.innerHTML = `<span class="badge">Ready</span>`;

6.3 メディアクエリ・条件適用

js
loadCSS('/css/print.css', { media: 'print' }); // 印刷時のみ loadCSS('/css/large.css', { media: '(min-width: 1200px)' });

6.4 代替スタイルシート(テーマ切替)

<link rel="alternate stylesheet" title="...">disabledプロパティを使い、テーマを切り替えできます。

js
function setTheme(title) { document.querySelectorAll('link[rel*="stylesheet"][title]').forEach(link => { link.disabled = (link.title !== title); }); }

7. よくある落とし穴

  • innerHTML<script>を書いても実行されない。必ず要素生成→DOM挿入。

  • 動的<script>順序保証が崩れる。依存関係があるならasync=falseで逐次ロード、またはimport()で制御。

  • CSSはレンダリングに影響しうるため、遅延しすぎるとFOUT/FOUCが発生。重要CSSは早めに、非クリティカルは遅延。

  • 何度も同じファイルを読み込むと副作用の重複(イベント多重登録等)。一意キーでの重複防止を徹底。

  • CSP/SRI設定と**crossoriginの整合**が取れていないと読み込み失敗。

8. 実用レシピ集

8.1 ビューポートに入ったら地図SDKを後読み

js
const mapBox = document.querySelector('#map'); const io = new IntersectionObserver(async ([entry]) => { if (entry.isIntersecting) { io.disconnect(); await loadScript('https://maps.example.com/sdk.js', { attrs: { integrity: 'sha384-...', crossorigin: 'anonymous' } }); initMap(); // SDKの初期化 } }, { rootMargin: '200px' }); io.observe(mapBox);

8.2 モジュール機能をオンデマンドで読込

js
document.querySelector('#exportBtn').addEventListener('click', async () => { const { exportAsCSV } = await import('/modules/exporter.js'); exportAsCSV(); });

8.3 テーマを即時切替(Constructable Stylesheets + 変数)

js
const sheet = new CSSStyleSheet(); sheet.replaceSync(`:root { --accent: #0ea5e9; } .btn { background: var(--accent); }`); document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; function setAccent(hex) { sheet.replace(`:root { --accent: ${hex}; } .btn { background: var(--accent); }`); }

8.4 クリティカルCSSはインライン、残りは遅延

html
<style> /* Above-the-fold の最小限 */ header { display:flex; gap:.5rem; } </style> <link rel="preload" as="style" href="/css/app.css" onload="this.rel='stylesheet'"> <noscript><link rel="stylesheet" href="/css/app.css"></noscript>

9. アンロード(解除)とクリーンアップ

  • スクリプトはDOMから要素を削除しても実行済みコードは残るため、グローバル副作用(タイマー、イベント等)を自前で停止する必要があります。

  • CSSは<link>要素を削除するか、disabled = trueで無効化できます。インライン<style>は要素を削除。

js
function unloadCSSByKey(key) { document.querySelectorAll(`link[rel="stylesheet"][data-key="${CSS.escape(key)}"]`) .forEach(n => n.remove()); }

10. まとめと指針

  • スクリプトはcreateElement(‘script’)または**import()**で、順序が必要ならasync=falseや逐次awaitで制御。

  • スタイルは<link rel=”stylesheet”>の動的追加、または<style>/CSSOM/Constructable Stylesheetsを使い分ける。

  • パフォーマンスは「必要な時だけ」「先読み」「重複防止」を徹底。

  • セキュリティはCSP・SRI・URLサニタイズを適用。

  • UXはFOUC対策、テーマ切替、条件適用を丁寧に。

ChatGPT5 生成日:2025/09/11