はじめに
とあるwebサイトの制作時に、CSSのfilter
プロパティでグレースケールのスタイルをあてた要素に、hover
時のスタイル(グレースケールを外す)をあてると意図した挙動になりませんでした(※iOS Safari のみ)。
そこで、will-change
プロパティにfilter
を指定したところ問題が解決しました。
実装コードは下記となります。
& a {
// will-changeでブラウザに変更予定を知らせる
will-change: filter;
filter: grayscale(1);
text-decoration: none;
color: #005bac;
display: block;
padding: 1em 1em 2.5em;
font-size: 1.4rem;
& img {
display: block;
margin-bottom: .5em;
}
&:hover {
transition: .25s;
filter: grayscale(0);
}
}
<li>
<a href="" target="_blank">
<img src="画像のパス" alt="">
</a>
</li>
will-change について
【MDN Web Docsの will-change 紹介ページ】ではこのように記されています。
CSS の will-change プロパティは、どのような要素の変更が予測されているかブラウザーに助言します。ブラウザーは要素が実際に変更される前に、最適化をセットアップすることができます。この種の最適化は、実際に変化が求められる前に、潜在的に高コストの処理を行うことで、ページの応答を向上させることができます。
サイト・ページを読み込んでくれるブラウザに「ここの要素にこういった変化があるからよろしく!」って先に伝えておくと、それに対して準備してくれる感じですね。
ブラウザレンダリングの仕組みも意識しておきたい
filter
の挙動不全に対してwill-change
での対応は早い段階で行って効果を得ていたのですが、レンダリングの影響もあったのかもしれないと思い、改めてブラウザレンダリングについても調べてみました。
ブラウザでは下記フェーズ・工程を通ってレンダリングしています。
- Parse(パース)
- Style(スタイル)
- Layout(レイアウト)
- Paint(ペイント)
- Composite(コンポジット)
Parse
では、HTMLとCSSが並行解析されて HTMLは「DOMツリー」、CSSは「スタイルルールズ」という構造になります。
次のStyle
では、各要素へのスタイルの割り当てが行われます。先ほど作られたDOMツリー(各ドキュメント要素)とスタイルルールズ(HTMLタグやclass, idなど各セレクタごとのスタイル)をリンクさせるフェーズです。
Layout
では、各要素の位置と大きさの計算を行います。
Paint
では、要素の重なり順を考慮し、順番に処理していくことで描画していきます。このフェーズでは Layer Tree(レイヤーツリー)が生成され、条件に応じてLayer(レイヤー)が分離されます。
Layer(レイヤー)を分離することで、もし何かスタイルの変更があった場合でもブラウザ側の手直しが最小となり、ブラウザの負担を軽減できます。
最後のComposite
では、今までの工程処理を進めてきたMain Thread
ではなくCompositor Thread
、Raster Thread
、GPU
といった別領域で進んでいきます。
Raster Thread
は4つあり、Compositor Thread
は空いているRaster Thread
にレイヤーのラスタライズ(画像化)を頼みます。そうしてラスタライズされたレイヤーをCompositor Thread
で一つのレイヤーに合成。最終的にGPU
に渡されて描画される流れです。
アニメーションの処理で top
, left
, margin
, padding
などではなく、transform
を使って指定したほうが良い、という話を聞いたことがありませんか?
実はtransform
やopacity
プロパティはこのComposite
フェーズで適用されます。
他方、top
, left
などを指定すると前工程(Layout
)に差し戻して処理し直す必要が出てきますのでブラウザに負担がかかってしまうのです。
Main Thread
は、Composite以外の工程に加えてJavaScriptの処理も担っていて多忙です。さらに、前工程に差し戻して処理を進める場合、CompositeもMain Thread
が一時的に担うことになります。
Main Thread
の負担を減らすようなブラウザに優しい記述を心がけたいですね。
ブラウザレンダリングの仕組みに言及し過ぎると本筋からずれてしまうと思いましたので、ざっと走り書きのような説明になってしまってすみません。
詳しい内容はこちらの【フロントエンジニアなら知っておきたいブラウザレンダリングの仕組みをわかりやすく解説!】サイトの記事が大変勉強になりましたので参考情報として掲載しておきます。
そもそもfilterプロパティはどのフェーズで適用されるの?
小見出しについて調べてみたのですが、filter
プロパティがどのフェーズで適用されるのかは見つけられませんでした。ご存知の方はコメントなどで教えていただけますと幸いです。
ただ、調べる中でこちらの【iOS SafariはCSSのfilterプロパティを使用すると重くなる】というサイトに興味深い記述がありました。
iOS SafariはCSSのfilterプロパティの処理を何故かGPUを使用せずにCPUで行います。そのため、filterプロパティを使用するとCPU使用率が高くなり、表示の乱れやレンダリング速度の低下、ブラウザが固まるなどの不具合が発生することがあります。
今回の件ではGoogle Chromeでは特に問題なく、実機テストした際にiOS Safariでの不具合に気づきました。そして冒頭のコード及び本記事の主題にあるようにwill-change
を使用することで解決できました。
今回当該スタイルを指定していた要素は複数個(10~16ほど)ありました。そのため、iOS SafariではCPUの負荷がかなり高かったのではないかと思います。
will-change
を使用したアニメーションプロパティーの指定があった場合、Paint
フェーズでレイヤーが分離されます。そのため、will-change
によって複数個ある要素の処理がレイヤーに分離されて進むことになった結果、少しでも処理が軽くなったのかなと考えています。
2024/12/04 追記
filter
はどうやらペイント (Paint) フェーズと合成 (Composite) フェーズの両方に関わってくるようです。
複数の生成AI(Chat-GPT
,Gemini
,Claude
,perplexity
)に聞いたところ、Paint または Composite という回答が多くありました。
参考程度に留めるべきかもですがGemini
の回答が最も具体的だったので記載しておきます。
filter プロパティは、一般的にペイント (Paint) フェーズと合成 (Composite) フェーズの両方に関わってきます。
具体的には、filter は要素の画像データに対して処理を行うため、ペイントフェーズで適用されます。 しかし、その結果は最終的な合成フェーズで他の要素と合成される際に考慮されます。 そのため、単一のフェーズに限定することはできません。 ブラウザによって実装が微妙に異なる可能性がありますが、ペイント後に合成フェーズの一部として扱われると考えて差し支えありません。
つまり、filter は要素がペイントされた 後 に適用され、最終的なピクセルデータが合成される前に修正を加える、という理解が適切でしょう。
--- Gemini 回答
まとめ
iOS Safariはbackground-attachment: fixed
が効かなかったり、今回のfilter
プロパティのこと(filterプロパティの処理を何故かGPUを使用せずにCPUで行う)だったり、独自の仕様を持っている時がありますよね。
今回はwill-change
のお陰で何とかなりましたが、background-attachment: fixed
の時は結局 JavaScriptを記述して無理やり力技で解決しました。
一つの不具合で思いがけないほど時間を費やしてしまうこともあるので、いろいろ引き出しを持っておくのは大切だなと感じます。
そして引き出しを増やすには基礎知識をはじめとしたインプットが重要だとも思いますので、冗長かと思いましたがブラウザレンダリングについても書かせていただきました。
筆者の備忘録的な部分もあるので読み進めにくい箇所もあったかもしれませんが、ここまでお読みいただきましてありがとうございました!
参考情報
MDN Web Docsの will-change 紹介ページ
フロントエンジニアなら知っておきたいブラウザレンダリングの仕組みをわかりやすく解説!
iOS SafariはCSSのfilterプロパティを使用すると重くなる
パフォーマンスに優しいCSSアニメーションとは