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 Components | React | Vue | Svelte |
|---|---|---|---|---|
| 学習曲線 | 低 | 高 | 中 | 中 |
| フレームワーク非依存 | ✓ | ✗ | ✗ | ✗ |
| 状態管理 | 手作業 | 容易 | 容易 | 容易 |
| パフォーマンス | 良好 | 中 | 良好 | 優秀 |
| 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年の答えじゃないでしょうか。