コンテンツにスキップ

CDN利用時の注意

CSSカスケードレイヤー導入時の、CDN経由のCSSの扱いについて説明します。

CDN利用時の問題点

@layer を導入したプロジェクトにCDN経由のCSSを取り込む場合、次のような問題が発生する可能性があります。

CDNのCSSは、通常 @layer が指定されていないことが多い(非レイヤーCSS)

非レイヤーCSSは、 @layer 管理されたCSSより優先度が高い

CDNのCSSを、 @layer 指定されたCSSから !important 無しで上書きできない

スライダーなどのプラグイン付属のCSSは、デザインによって上書きすることが多いため、CDNを利用する場合は、上書きの方針を定めておく必要があります。
!important 利用については「解決方法-3」で詳しく述べます。

取り込むファイルが @font-face 指定などの「実質グローバルになるアットルール」に該当し、上書きが必要ない場合はこの限りではありません。

CDNの利用方法

CDNのCSSをHTTP経由で取り込む方法は、次の2つのうちどちらかになります。

  1. CSSファイルから @import で取り込む
  2. HTMLファイルから link タグで取り込む

1・@import で取り込む

1 は、 @import url("https://cdn...") layer(...); のように、layer() 付きの @import で取り込む方法です。
読み込みが直列になることによるパフォーマンス面を許容できる場合、ネイティブCSSと PostCSS(postcss-import)では、CDNのCSSを直接レイヤーに所属させられます。
この場合、@import@layer の宣言文の直後(他のルールより前)に記述する必要があります。

一方、Sass では利用できません。Sass はプレーンCSSの @import を出力ファイルの先頭に移動するため、@layer の宣言より先にインポート先のレイヤーが登録され、宣言した順序が壊れます。

2 は、一般的なCDNの利用方法です。
以下から、2 の方法について、問題を整理して対策方法をまとめます。

CDNのCSSをHTTP経由で直接取り込まずに、ファイルとしてコピー配置する場合は、 外部製CSSの取り込み をご確認ください。

@layer 導入時のHTMLでの読み込み

プロジェクトのCSSを全て @layer に所属させている状態において、CDN経由で非レイヤーのCSSをHTMLから取り込む場合を考えてみます。

一般的なHTMLでの読み込み順は、次のようになります。

HTML|読み込み順の例
<!-- 1・CDN経由のCSS(内部は非レイヤーのCSS) -->
<link rel="stylesheet" href="https://cdn.example.com/plugin.css">

<!-- 2・全域用(内部は全て@layer指定済) -->
<link rel="stylesheet" href="/assets/css/global.css">

<!-- 3・ページ個別用(内部は全て@layer指定済) -->
<link rel="stylesheet" href="/assets/css/pages/product.css">

上記では、CDNのCSSを最初に読み込んでいます。
通常であれば、後続する 2 と 3 のファイルでCDNのCSSを上書きできます。
しかし、@layer の導入環境下では、ファイルの読み込み順序に関係なく、必ず非レイヤーのCSS(1のCDNのCSS)の方が、@layer 指定のCSS(2 と 3)より優先されます。

つまり、この状態では、いくら 2 と 3 のCSSファイルで、1 に対する上書き用のCSS(通常のスタイル)を書いても効きません。

この問題の解決方法は3つあります。

解決方法-1: 上書き用のCSSを追加で読み込む

次のように、1の後に、同じ非レイヤーCSSを読み込んで、そのファイルから上書きします。

HTML|上書き専用のCSSを読み込み
<!-- CDN経由のCSS(非レイヤー) -->
<link rel="stylesheet" href="https://cdn.example.com/plugin.css">

<!-- 非レイヤーCSS上書き用(非レイヤー) -->
<link rel="stylesheet" href="./assets/css/override-cdn.css">

CSS|override-cdn.css の記述例
@charset "UTF-8";

/* CDN の plugin.css を上書き */
.plugin-element {
    color: #333; /* 非レイヤー同士なので後で読み込むCSSで上書きする */
}

上書きの役割を明確に分離できますが、広範囲に及ぶ場合は管理が煩雑になります。

解決方法-2: CSS内部の記述を工夫して解決する

解決方法-1のように別途ファイルを読み込むことなく、 global/pages/ に配置したCSS内部に、CDNの上書き用のCSSを記述する方法です。

部分的に @layer のブロックから外して「非レイヤー」のCSSとして記述します。

CSS|非レイヤーの上書き例(Pagesの場合)
@charset "utf-8";

/* CDN の plugin.css を非レイヤーとして上書き */
.plugin-element {
    color: #333;
}

/* @layerのCSS */
@layer pages {
    .object-1 { ... }
    .object-2 { ... }
    .object-3 { ... }
}

上記は pages/ のCSSですが、global/ の場合は、global.css にて、概念レイヤーを包括的に @layer に所属させる方法を標準としています。
各パーシャルファイルのCSSは必ず @layer に所属するため、別途、非レイヤー上書き用のファイルを作成し、追加で読み込む必要があります。

global.scss | 非レイヤーのCSSを記述したパーシャルファイルを読み込み
@charset "utf-8";

@use "sass:meta";

@layer base , layout , components , pages , utilities , print;

/* 各レイヤーの `_index.scss` を読み込み */
@layer base { @include meta.load-css("./global/base/"); }
/* 〜中略〜 */
@layer print { @include meta.load-css("./global/print/"); }

/* CDN上書き用の `_override-cdn.scss`(非レイヤー)を読み込み*/
@include meta.load-css("./global/override-cdn");

どちらの場合も、従来のカスケード仕様で「後勝ち」になるよう、HTMLでは先にCDNのCSSを読み込んでおく必要があります。(冒頭の ”読み込み順の例” のコードを参照)

解決方法-3: !important で上書きする

@layer 内のCSSに !important を付ければ、対策をしなくても非レイヤーのCSSを上書きできます。これは、次の仕様によるものです。

  • CDNが通常宣言なら、!important は無条件で勝つ
  • CDNが !important付きでも、!important 同士はレイヤーの強弱が反転する
    レイヤー所属の !important がレイヤー外の !important に勝つ
CSS|!importantで上書きしている例
@layer pages {
    .plugin-element {
        color: #333 !important; /* 技術的に問題なく優先される */
    }
}

1〜数か所だけの場合や、今すぐ直したい緊急対応の場合に有用です。
常態化してきたら、解決方法-1、2 への置き換えを推奨します。

将来的な対応(参考情報)

将来、CDN利用の問題はスマートに解決できるようになるかもしれません。

HTMLの <link> 要素において、直接 layer 属性を指定する方法です。

HTML|linkタグにlayer属性を記述した例
<link rel="stylesheet" href="https://cdn.example.com/plugin.css" layer="library">

参考: CSSWG で議論中の提案(#5853)

まだCSSWGでも提案段階の状態のようですが、問題なく利用可能になった場合は、CDNのCSSをHTMLで読み込んだ段階で、任意のCSSレイヤーに組み込めます。