Shadow DOM と CSS Containment によって実現されたCSSのスコープの概念

UI開発者 宇賀

CSSにはスコープという概念が存在しません。これはCSSモジュールを設計する時だけでなく、JavaScriptウィジェットを作成する時にも重要な懸案事項です。

たとえば、クラス名などのセレクタにもスコープは存在しないため、カジュアルに衝突が起こります。その衝突を常に考慮しながらCSS設計を行う必要があり、その手段としてOOCSSBEMSMACSSFLOCSSなどの命名規則が今も議論されています。

他にも、あらゆるスタイリングがスコープを持たないことから、製作者の意図しないカスケーディングが発生する可能性があります。

そのため、JSウィジェットがCSSモジュールに影響され、CSSモジュールがJSウィジェットに影響されるということが起きやすく、開発者自身の注意深さでそれらを回避するという方法しかありませんでした。

そんな中、CSSセレクタのスコープを実現する方法が 2013年に Google Chrome 25 にプレフィックス付きで実装されたことで誕生しました。最近ではすっかり有名になったShadow DOMです。

Shadow DOM

Shadow DOMは手軽にCSSのスコープを生成することができ、DOMのカプセル化を実現することができる技術です。

最近だと 2016年9月にリリースされた Google Chrome 53 で Shadow DOM V1 がサポートされました。Firefox でも動作確認は可能です[1]

たとえば、以下のような見出しモジュールが存在するとします。

見出しレベル1

この見出しモジュールをShadow DOMの中に入れることで、元々のCSSの影響を受けなくなります。

JavaScript無効環境、あるいはShadow DOM に対応していないブラウザです。Shadow DOM がサポートされているGoogle Chromeで閲覧してください。

HTML 上は全く同じマークアップですが、Shadow DOM であるため、Shadow DOM 内のCSSは Shadow DOM のルートの外に影響せず、また外部からも影響されません。基本的にブラウザが用意したCSSが適用されます。今回の例では .hdg_l1_1 を持つ要素の文字色を変更させるCSSが書かれた style 要素も Shadow DOM の中に追加していますがShadow DOMの外には影響していないことが実際に確認できます。

Shadow DOM は JavaScript を前提とした機能ですが、元々がJavaScriptで実装されるウィジェットが他のCSSに影響されず、他の要素に影響させることを防ぐために誕生したものです。これは、多くのWebアプリケーションやWebサイトで用いられる機能の開発を助けます。

例として、以下のようなソースを示します。以下をHTMLファイルに貼り付けて読み込むことで Shadow DOM を実現できます。

<template id="template">
<style>
  p {
    color: tomato;
  }
</style>

<p>Hello World</p>
</template>

<div id="shadow-host">JavaScript無効環境、あるいはShadow DOM に対応していないブラウザです。Shadow DOM がサポートされているGoogle Chromeなどで閲覧してください。</div>

<script>
var shadowHost = document.querySelector('#shadow-host');
var shadowRoot = shadowHost.createShadowRoot();
var template = document.querySelector('#template');
var clone = document.importNode(template.content, true);

shadowRoot.appendChild(clone);
</script>

これは、template 要素からDOM構造を複製し、div 要素の中に Shadow DOM を構築するサンプルです。

Google Chrome の DevTool で要素を検証すると、まるで iframe 要素のような振る舞いをしていることがわかります。

Google Chromeでは、DevToolで「Show user agent shadow DOM」にチェックを入れると、ブラウザが用意している要素のShadow DOMを閲覧できます。たとえば、input 要素の placeholder 属性などです。以下の input 要素をGoogle Chrome で検証することで、placeholder や、入力されたテキストがどのようにして表示されているかがわかります。

CSS Containment

「スコープが存在しない」というのは、技術者だけでなくブラウザにとっても厄介です。 なぜならCSSはセレクタだけでなく、ブラウザが計算する際に考慮すべき範囲にもこれまでスコープを示す手段が存在しないからでした。

ブラウザは、要素を配置(配置領域の計算を)してから描画(ペイント)するという大きく分けて2段階の流れを汲んでいます。 要素を配置は、Webkit 系ブラウザで「レイアウト」、Gecko 系ブラウザで「リフロー」と呼ばれています。

これらはJavaScriptなどでスタイルが追加、削除されたり、DOM操作が行われた時や、ウィンドウのリサイズなどにも発生します。

たとえば、以下のようなソースがあったとします。

<div class="example-01">
<ul class="entry-list">
<li>
<a href="dummy">
<div>
<!-- title や description など -->
</div>
</a>
</li>

<li>
<a href="dummy">
<div>
<!-- title や description など -->
</div>
</a>
</li>

<li>
<a href="dummy">
<div>
<!-- title や description など -->
</div>
</a>
</li>
<!-- /.entry-list --></ul>
<!-- /.example-01 --></div>

この時、.example-01 > ul > li:first-child > a に要素の追加を試みます。

<div class="example-01">
<ul>
<li>
<a href="dummy">
<b>既読</b>
<div>
<!-- title や description など -->
</div>
</a>
</li>

<li>
<a href="dummy">
<div>
<!-- title や description など -->
</div>
</a>
</li>

<li>
<a href="dummy">
<div>
<!-- title や description など -->
</div>
</a>
</li>
<!-- /.entry-list --></ul>
<!-- /.example-01 --></div>

この時に新たに挿入された b 要素が、「サイズ固定で且つ position: absolute; な要素」だった場合、 他の要素のレイアウトや描画は、考慮したり再計算する必要は無いはずですが、レイアウト・描画を行う時の影響範囲をブラウザに伝える手段が無いため、他の無関係な要素、DOM全体にも計算処理が新たに実施されてしまいます。

これはブラウザにとって、無駄な仕事でしかなく、ユーザーにとっても意味の無い時間でしかありませんでした。

そんな中、2016年7月にリリースされた Google Chrome 52 で CSS Containment 実装されたことが話題となっています。

CSS Containment とは、「CSSの封じ込め」を実現するCSSプロパティです。DOMの一部を操作する際に実施される無駄な再計算を抑制することでパフォーマンス向上に期待できます。

content プロパティの値と概要
概要
strict layout, style, paint, size を指定するのと同義です。
content layout, style, paint を指定するのと同義です。
layout 要素が不透明であることをブラウザに保証し、要素外のコンテンツがこの要素に影響したり、この要素が外のコンテンツに影響することはありません。
また、要素はブロック整形文脈を作ります。
たとえば、子要素に存在する要素が浮動要素(float: left; など)のフロートを解除します。
paint 要素の子孫が要素の領域外にレンダリングされないことを保証します。要素はレイアウト上完全に独立したものとして考えられ、絶対配置(position: aboslute;)や固定配置(position: fixed;)された要素の表示位置の基準となる包含ブロックとして機能します。
また、要素はブロック整形文脈を作り、同時にスタック文脈も作ります。
style 一部のCSSプロパティは、要素の子孫に影響しなくなり、また子孫要素にスタイルの変更における影響範囲のスコープが限定されます。
size 要素のサイズ計算が、子孫要素を考慮せずにレイアウトされることが保証されます。そのため、他のCSSの効果や、height プロパティなどで直接高さを指定しない限り要素のサイズは 0 px になります。

※ 保証するというのは、画面上に描画される結果に対してではなく、ブラウザに伝わるレイアウト・描画の計算についてです。

layout は最も恩恵を受けることができる値で、paint は最も強力な封じ込めを実現します。ブラウザにとって、無駄な仕事を減らすことで複雑なウェブページのパフォーマンス向上に期待できます。

スコープの概念を獲得していくCSS

Shadow DOM や CSS Containment などの登場に加えて、執筆時点ではまだ実験段階の機能ですが、2008年に提案書が公開された CSS Variables もスコープの概念を持っています。このように、あらゆる方面に対して CSSは次々とスコープの概念を獲得して来ています。

各ブラウザベンダーによる実装の動向を慎重に見守りながら、用途に応じて積極的にこれらの技術を使っていくことで、今後の制作物のパフォーマンスと品質向上により一層期待できるでしょう。

  • [1]:Firefox では about:config の dom.webcomponents.enabled 設定を true に設定することで実行結果は確認できます(確認環境:Firefox 49.0.1)。