レ点腫瘍学ノート

日記/2021年/3月8日/Lazysizes.jsを使うとCLSが発生する問題(解決済) の履歴差分(No.9)


#author("2021-10-17T20:24:09+09:00;2021-03-08T19:34:03+09:00","","")
#author("2021-10-17T20:45:36+09:00;2021-03-08T19:34:03+09:00","default:tgoto","tgoto")
#ref(https://oncologynote.com/img/16f4ac0c0a.jpg,nolink)

lazyload(画像の遅延読み込み)するためにlazysizes.jsを使うとスペースホルダーによりCLS (Cumulative Layout Shift)が発生する問題に関して記載するページです。このサイトも遅延読み込みのためにlazysizes.jsを使ってスペースホルダーを使うとCLSが発生する問題について長らく悩まされてきました。lazyloadは現代WEBには欠かせない技術ですが、ようやくひとまず解決できたのではないかと思えるところまで到達したので、今回の対応策を記載しておきます。

>''追記(2021/6/21)'':その後に問題が生じて再びCLSが生じるようになってしまいました。詳しくは[[こちらの記事>https://oncologynote.com/?0323aec317]]をご覧ください。

#contentsx(depth=1)

*CLS(Cumulative Layout Shift)とは [#v61d1a59]

CLSとはコアウェブバイタルの1つの要素で、SEOのために非常に重要な因子です。ページの読み込み中に表示されるページ内要素が、読み込み完了までにずれることを指し、ユーザー体験を悪化させるものとしてできるだけCLSが出ないようなレイアウトをすることが求められています。このCLSはpagespeed Insightsの評価要素にもなっており、CLSが残ったまま放置されているとSEOにとって悪影響なのです。

#ogp(https://web.dev/cls/)

CLSを無くすためには様々な方法を組み合わせて対処する必要がありますが、とくに画像のレスポンシブ対応ではCLSに影響するものがたくさんあるので、画像のHTMLをどう出力するかには気を遣う必要があります。lazyload(画像遅延読み込み)やwebp fallbackと両立させるためにはなおさらです。

#ogp(https://webtan.impress.co.jp/e/2020/06/05/36210)

*lazysizes.jsでは当初CLSにどのような問題が生じていたか [#g4e4bec6]

lazysizes.jsはlazyload(画像遅延読み込み)のために非常に人気のあるスクリプトですが、CLSが発生するという問題がありました。CLSがあるとSEOにとって好ましくないので、SEOのためにlazysizes.jsを使っているサイトにとっては本末転倒です。

もともとこのサイトはPukiwikiを使っていますが、Pukiwikiでは画像を出力するのにref.inc.phpを使用します。ref.inc.phpでは画像の入力に対してHTMLを吐きますが、lazyloadおよびwebp fallbackに対応するために当サイトのref.inc.phpは下記のようなHTMLを出力していました。下記は横750 X 縦500の画像サイズの例です。

 img{
 	width:100%;
 	height:auto;
 }

 <picture>
 <source type="image/webp" data-srcset="sample.webp">
 <img class="lazyload" src="spaceholder.gif" data-src="sample.jpg" width="750" height="500">
 </picture>

#ogpi(https://oncologynote.com/?5b041d2a93)

しかしこれでは、ダミーgifファイルが1X1サイズなので、一瞬だけ750X750のサイズとして表示されてしまいlayout shiftが生じます。つまりこの画像ファイルが遅延読み込みされる間、画像よりも下にある文章などがいったん低い位置に表示され、画像の読み込みが完了した時点で250px分だけずれる(CLSが生じる)ことになるのです。

#ogp(https://escaper3rx3air.blog.fc2.com/blog-entry-125.html)

webp fallbackがない場合でもやはり同様です。webp fallbackを必要としない場合は下記のようなHTMLになると思いますが、やはりプレースホルダが1X1サイズなので一瞬だけ高さが750と表示されてしまい、250px分のLayout Shiftが発生してしまうのです。

 <img class="lazyload" src="spaceholder.gif" data-src="sample.jpg" width="750" height="500">

もともとimgタグの中にwidth="XXX" height="XXX"を記載しているのはレスポンシブ対応にしたときにCLSが発生しないようにするためのもので、lazysizesを使っていない場合はこのwidthとheightでLayout Shiftが発生しなくなるのですが、lazysizes.jsを使っている場合はdata-srcの読み込みが終了するまでの間、widthとheightが無視されてしまい、どうしてもLayout Shiftが発生してしまいます。

CLSを無くしたいのですが、これについては対応をググってもなかなか良い方法が見つけられませんでした。

*lazysizes.jsでCLSを無くすために当サイトがとった解決策 [#mbd969b8]

#ogp(https://hibidesign.com/memo/web/181)

長らく困っていたのですが、ようやく上記のサイトを見つけて、lazysizes.jsに''ls.aspectratio.min.js''というプラグインがあることに気がつきました。これはどうやらlazysizes.jsで読み込む画像にアスペクト比を反映させるプラグインのようです。このプラグインをlazysizes.jsのあとに読み込ませて、imgタグにはdata-aspectratio="750/500"を記載してやります。

 <picture>
 <source type="image/webp" data-srcset="sample.webp">
 <img class="lazyload" src="spaceholder.gif" data-src="sample.jpg" data-aspectratio="750/500">
 </picture>

こうすると、なんとLayout Shiftが発生しなくなりました。悩み始めてから約1年かかってしまいましたが、ようやく解決するに至ったのでした。

*lazysizes.jsのプラグインも一括してjsdelivrで読み込む方法 [#l520e591]

jsdelivrでlazysizes.jsを読み込んでいる場合は、プラグインも一括で読み込むことができます。HTMLに下記のタグを記載しておくだけです。

 <script src="https://cdn.jsdelivr.net/combine/npm/[email protected],npm/[email protected]/plugins/aspectratio/ls.aspectratio.min.js" async></script>

当サイトはdiv要素などでも遅延読み込みを可能にするためのunveilhooksプラグインも使用しているので、これも含めて一括で読み込む場合は下記の通りになります。

 <script src="https://cdn.jsdelivr.net/combine/npm/[email protected],npm/[email protected]/plugins/unveilhooks/ls.unveilhooks.min.js,npm/[email protected]/plugins/aspectratio/ls.aspectratio.min.js" async></script>

なお、Pukiwikiのref.inc.phpは下記のようになります。

-「data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7」は1X1pxのサイズのダミーgifファイルで、これをプレースホルダとして使っています。
-WEBP画像の存在確認をfile_existsで行うために外部URLであればstrposとstr_replaceを使って相対パスに切り替えています。

 if ($is_image) { // 画像
   if ( PLUGIN_REF_WEBP_FALLBACK ) {
     if(strpos($url,$script) !== false){
       $url = str_replace($script, '', $url);
     }
     $urlwebp = pathinfo($url, PATHINFO_DIRNAME ) . '/' . pathinfo($url, PATHINFO_FILENAME ) . '.webp'; //同じディレクトリにwebp画像があるか探す
   }
   if ( PLUGIN_REF_WEBP_FALLBACK && file_exists($urlwebp) ) {
     $params['_body'] = "<picture><source type=\"image/webp\" data-srcset=\"$urlwebp\"><img class=\"lazyload refimg\" src=\"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\" data-src=\"$url\" alt=\"$title\" data-aspectratio=\"$width/$height\"></picture>"; //webp画像fallback
   } else {
     $params['_body'] = "<img class=\"lazyload refimg\" src=\"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\" data-src=\"$url\" alt=\"$title\" $info>";
   }
   if (! $params['nolink'] && $url2) {
     $params['_body'] = "<a href=\"$url2\" title=\"$title\">{$params['_body']}</a>";
   }
 } else {
   $icon = $params['noicon'] ? '' : FILE_ICON;
   $params['_body'] = "<a href=\"$url\" title=\"$info\">$icon$title</a>";
 }

*その後に生じた問題 (2021/6/21追記) [#ia5e513e]

#ogpi(https://oncologynote.com/?0323aec317)

#ogpi(https://oncologynote.com/?5b041d2a93)
#ogp(https://amzn.to/3rvWRcG)