Web Components1年本番運用で踏んだ地雷——Shadow DOMとTailwindの相性問題から本当の使い道まで

Web Componentsを1年間本番運用してわかった、Shadow DOMの落とし穴とTailwind CSSとの相性問題。フレームワーク連携の現実と、実装例で見つけた本当の使い道を実体験から解説します。

Web Componentsと本気で向き合った1年の話

先日プロジェクトで、フレームワーク非依存なUIコンポーネント化を狙ってWeb Componentsを導入したんですよ。「マイクロフロントエンドの夢」みたいな理想を掲げてね。結果、地獄を見ました。でも今は本番環境で堅実に運用できてる状態なので、その1年間で踏んだ地雷と、実際に使える場面をぶっちゃけていきます。

Shadow DOMは「封印」じゃなく「刃物」だった

最初、Web Componentsの魅力ってShadow DOMのカプセル化だと思ってました。グローバルCSSから隔離されて、スタイル汚染なし—理想的だ、と。

でもね。本番で2週間運用したら、地獄の入り口が見えました。

うちのチームではTailwind CSSを使ってるんですが、Shadow DOM内ではTailwindが効かないんです。当たり前っちゃ当たり前なんですけど、「あれ、pt-4 が効いてない」って気づいた時の絶望感ったら…。

class MyButton extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        /* これだけでは不十分 */
        :host {
          display: inline-block;
        }
        button {
          padding: 0.5rem;
          background: white;
          border: 1px solid #ccc;
        }
      </style>
      <button><slot></slot></button>
    `;
  }
}
customElements.define('my-button', MyButton);

で、どう対策したかというと、CSSカスタムプロパティをめっちゃ使うに落ち着きました。親のスコープから値を流し込む方式です。

class MyButton extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        button {
          padding: var(--btn-padding, 0.5rem);
          background: var(--btn-bg, white);
          border: var(--btn-border, 1px solid #ccc);
          font-size: var(--btn-font-size, 1rem);
        }
      </style>
      <button><slot></slot></button>
    `;
  }
}

親側でこう使う:

<my-button style="--btn-padding: 1rem; --btn-bg: #007bff;">Click me</my-button>

これでようやく親のスタイルシステムと共存できました。ただね、これやると「カプセル化の利点」の半分以上が吹っ飛びます。正直、最初の理想と現実のギャップが大きかったんですよ。

フレームワークとの相性は「仲良し」じゃなく「同居人」レベル

React・Vue・Svelteとの連携を検証したんですが、やっぱり完全には統合できません。そういうもんなんだと割り切ることが大事です。

Reactとの組み合わせが一番辛かったです。Reactは仮想DOMで管理するのに、Web Componentsは実DOMだから相性が微妙。propsの受け渡しがスムーズじゃないんですよ。

// これはうまく動かない場合がある
const MyComponent = () => {
  const [value, setValue] = useState('hello');
  return <my-button value={value}>Click</my-button>;
};

// こっちならうまくいく
const MyComponent = () => {
  const buttonRef = useRef();
  const [value, setValue] = useState('hello');
  
  useEffect(() => {
    if (buttonRef.current) {
      buttonRef.current.setAttribute('value', value);
    }
  }, [value]);
  
  return <my-button ref={buttonRef}>Click</my-button>;
};

Vue 3のほうがまだマシですね。v-modelの仕組みがWeb Componentsの双方向バインディングと相性がいい。ただしSvelteはもっと相性がいい。リアクティビティのモデルがWeb Componentsの思想に近いんでしょう。

実務的には、「Web Componentsはフレームワークに依存しない」という触れ込みは、正確には「フレームワークと疎結合」に過ぎないんだと気づきました。完全に独立させるなら、フレームワークなし環境で使うのが一番ストレスがありません。

Slotとイベントリスニングの落とし穴

<slot>はめっちゃ便利なんですが、ネストが深くなると地獄になります。

<!-- 親コンポーネント -->
<my-card>
  <div slot="header">
    <my-button>
      <span>nested content</span>
    </my-button>
  </div>
</my-card>

こんな感じでネストするとね、イベントのバブリングが予想と違う動きをするんです。Shadow DOMの境界を越えるとイベントが「マスクされる」という話があって、実装初期はこれでハマりました。

正直、深いネストが必要なUIなら Web Componentsでカプセル化するのは見直したほうがいい。浅い階層の「再利用可能なUIパーツ」に絞ったほうが吉です。

うちの現在の使い分けは以下の通り。単層的なUIはWeb Componentsで、複雑な構造はフレームワークに任せるイメージです:

Web Componentsで実装するもの

  • ボタン、フォーム入力、モーダル、タブ(単層的なUI)

フレームワーク(Vue)で実装するもの

  • ページ全体の構成、複雑なロジック、ネストが深いUI
<!-- good: 単純で再利用性高い -->
<my-button variant="primary">Submit</my-button>
<my-input type="email" placeholder="Email"></my-input>
<my-modal title="Confirm">Are you sure?</my-modal>

<!-- avoid: ネストが深い、ロジック多い -->
<my-complex-form>
  <my-form-section>
    <my-field>
      <my-input></my-input>
      <my-error-message></my-error-message>
    </my-field>
  </my-form-section>
</my-complex-form>

ブラウザ互換性とpolyのジレンマ

2026年だからWeb Componentsの主要APIはほぼ全ブラウザ対応してるんですけど、細かいバグは結構あります。地味に困るんですよね。

幸い IE11 サポートは終了してるので、Chrome・Firefox・Safari・Edgeではほぼ問題ありません。ただしEdgeのChromium版でも微妙なバグがあって、それは本番で初めて気づいたりするんです。「あ、本番環境でだけ変な動き…」みたいな。

2026年時点で「Web Componentsを使うべき場面」と「避けるべき場面」

1年運用した結論として、こう整理しました。正直、最初の理想とはかなり違う結論に到達したんですけど、これが現実的な答えだと思います。

使うべき場面

  • マイクロフロントエンド(複数プロジェクトで共有するUIパーツ)
  • デザインシステムの実装(再利用性重視)
  • フレームワーク非依存なUIライブラリ
  • 既存jQueryサイトへの組み込み

避けるべき場面

  • SPA全体の構成
  • ネストが深いUI
  • リアルタイム更新が頻繁な部分
  • Redux/Vuexなどの状態管理と連携が必要な場合

以下が判断フローです。これに沿って進めると、余計なトラブルは大幅に減ります:

graph TB
  A[Web Components導入判断] --> B{再利用性が高い?}
  B -->|yes| C{フレームワーク非依存が必要?}
  B -->|no| D[フレームワーク採用を検討]
  C -->|yes| E{ネスト深さは浅い?}
  C -->|no| D
  E -->|yes| F[Web Components推奨]
  E -->|no| G[フレームワークのほうがいい]
  F --> H[実装開始]
  G --> D

パフォーマンスと初期化の遅延地獄

Custom ElementsのconnectedCallbackはDOMに追加されるたびに呼ばれるんですが、これが重いと本番で痛い目を見ます。うちはバーチャルスクロール対応のテーブルで1000行ごとのコンポーネント初期化によるFCP遅延で悩まされました。2秒待たされるのは結構なストレスなんですよ。

対策としては、初期化処理を遅延させるパターンを導入しました:

class MyComponent extends HTMLElement {
  connectedCallback() {
    // 軽い処理だけここでやる
    this.render();
    
    // 重い処理は次のフレームで
    requestIdleCallback(() => this.initialize());
  }
  
  render() {
    this.innerHTML = `<div>loading...</div>`;
  }
  
  initialize() {
    // 重い初期化処理
    this.setupEventListeners();
    this.loadData();
  }
}

これでFCPは0.3秒に改善されました。ただし、初期化待ちにUIが反応しない期間があるので、スケルトンローディングなどのUX対策は必須です。

Web Componentsvsフレームワークの比較

項目Web ComponentsReactVueSvelte
学習曲線
フレームワーク非依存
状態管理手作業容易容易容易
パフォーマンス良好良好優秀
IDE サポート限定的優秀優秀優秀
デバッグ難しい容易容易容易
マイクロフロントエンド✓✓

本番運用で学んだ地味だけど重い工夫

属性変更の監視が大事

正直、ここを雑にすると無駄なレンダリングが多発します。

class MyComponent extends HTMLElement {
  static get observedAttributes() {
    return ['value', 'disabled', 'variant'];
  }
  
  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue === newValue) return; // 不要な再レンダリング防止
    this.update();
  }
}

デバッグが地獄だからChrome DevToolsの活用が必須

Shadow DOMの中身を確認するには、DevToolsのSettingsで「Show user agent shadow DOM」を有効にする必要があります。これを知らずに2時間無駄にしたチームメンバーがいます…。地味に重要な設定です。

イベントのバブリングをちゃんと理解する

Shadow DOM内部で発火したイベントは、デフォルトではShadow DOMの外に出ません。外に出したければcomposed: trueが必須です。これがないと親要素で捕捉できず、「なぜかイベントが反応しない…」という謎現象が発生します。

this.dispatchEvent(
  new CustomEvent('my-event', {
    detail: { value: 'hello' },
    bubbles: true,
    composed: true  // これがないと親要素で捕捉できない
  })
);

2026年の現状:Web Componentsは「万能薬」ではなく「適材適所」

Web Componentsは確かに便利です。でもそれは、正しい場面で使ったときだけなんですよ。

最初はマイクロフロントエンドの理想に魅せられて、できるだけWeb Componentsで統一しようとしました。6ヶ月経って「あ、これ違う方向だ」に気づいて、設計を半分戻しました。正直、無駄な時間もありましたが、そこから学んだことが多い。

いま思うのは、Web Componentsは「デザインシステムのボーダーレイヤー」として使うのが最適だということです。つまり、複数プロジェクトや複数フレームワークで共有するボタンやフォーム入力などの、粒度の細かくて再利用性の高いUIパーツ。

一方で、ページ全体の構成や複雑なロジックは、フレームワークで統一したほうが保守性も開発効率も良いんです。無理にWeb Componentsで統一しようとするから地獄が生まれるんじゃないでしょうか。

まとめ

  • Web Componentsの地雷:Shadow DOMのカプセル化は想像より複雑。CSSカスタムプロパティでの調整は必須。フレームワークとの相性は「独立」というより「共存」
  • 実装すべき場面:マイクロフロントエンド・デザインシステム・フレームワーク非依存な再利用パーツ。ネストが浅い単層的なUIに限定する
  • 避けるべき場面:SPA全体・複雑なロジック・ネストが深い構造・リアルタイム更新が頻繁な部分
  • パフォーマンス:初期化処理は遅延実行。バーチャルスクロールなど大規模レンダリング時はrequestIdleCallbackでボトルネックを回避
  • デバッグと保守:イベントのcomposed: true設定・Shadow DOMのDevTools確認・属性変更のobservedAttributes設定が重要。これらを理解してない本番運用は地獄です

正直、Web Componentsは「ハイプサイクルの頂上」を越えた今だからこそ、現実的な使い分けができる段階だと思います。理想を追わず、プロジェクトの制約に合わせて導入する—それが2026年の答えじゃないでしょうか。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事