📖

Next.jsで画像が読み込み終わったらCSSクラスを外す処理【備忘録】

2022/01/15に公開
1

いい感じにしてくれるImage

Next.jsではこんな感じで書くと

 <Image
	width={2000}
	height={2000}
	src={value}
/>

画像を最適化して配布してくれますよね。
遅延読み込みもしてくれるので大変ありがたいのですが、大きい画像をSwiperのようなスライダー系で読み込むと
「カルーセルが次に移動してもしばらく画像なしで、少しして画像が表示される」
といった若干違和感を覚える挙動になってしまいます。

遅延読み込みをやめればいいのですが、早さ的には避けたいものです。

画像の読み込みが終わったらCSSを外したい

読み込みのクルクルするCSSを用意しておいて、画像の読み込みが終わったら、そのCSSを外す的なことができれば、上記の違和感を軽減できると考えました。

つまり
onloadみたいなイベントハンドラで読み込み用CSSクラスを外す処理を書きたい!

imagesLoadedを使ってみたがダメでした

画像の遅延読み込みのハンドリングが簡単になるライブラリ「imagesLoaded」を使ってみました。
https://www.npmjs.com/package/react-images-loaded

react用もあるので使ってみたのですが、Image内画像だと読み込みが終わる前に読み込みされた判定になってしまいました。

私の力不足の可能性もありますが……Next.jsのImageでの画像だとだめなのかもしれません……

Next.js v11.1.0で追加されたonLoadingComplete

タイトルのとおりです。
https://nextjs.org/docs/api-reference/next/image

v11.1.0 onLoadingComplete and lazyBoundary props added.

めちゃくちゃそのまんまのものが追加されていました。

Image内部にonLoadingComplete={loadedImage}を追加して、

 <Image
	width={2000}
	height={2000}
	src={value}
	className="img-loading"
	onLoadingComplete={loadedImage}
/>

読み込み終わった時にCSSのクラスを外す処理を書けば

const loadedImage={
}

あれ、、これって画像のgetElementできなくないか??
どうやって画像のidなどを拾ってくるの……?

ドキュメントを見ると

The onLoadingComplete function accepts one parameter, an object with the following properties:

naturalWidth
naturalHeight

なのでこうしても

  const loadedImage = (e) => {
    console.log(e);
    }

{naturalWidth: 1118, naturalHeight: 732}

widthとheightしか帰ってこないんですね。肝心の要素が取得できません。

これだとどこのクラスを引っこ抜けばよいかをJSに渡せない……

jsを活かす

あくまで私のケースですが、このように書いていたんです。

 <Swiper tag="nav" {...params}>
        {props.image_urls.map((value, key) => {
          return (
            <SwiperSlide key={key}>
              <Image
                width={2000}
                height={2000}
                src={value}
                id={"img_" + key}
                objectFit="contain"
                className="img-loading"
		onLoadingComplete={loadedImage}
              />
            </SwiperSlide>
          );
        })}
      </Swiper>

なので、この関数呼び出してる部分を変更して

onLoadingComplete={loadedImage}

onLoadingComplete={() => {
	let imageElement = document.getElementById("img_" + key);
	console.log(imageElement);
}}

画像のidを取れちゃいます!!! ありがとうES2015。

私の場合はforループでSwiperの中身を出していたためこの手法ができました。

謎の挙動の原因。Swiperのloopに注意。

当初イベントハンドラがうまく機能しませんでした。消したはずのclassが存在しています……

もしかしてNext.jsのバグを発見しちゃったのかななんて思ったのですが、色々操作しているとそうではありませんでした。

Swiperのloop機能によって同じidを持つimg要素が爆誕していたのです。
https://zenn.dev/attt/articles/swiper-loop-without-duplicate
Swiperのスライドの端に来たときに次押すと最初に戻る機能がloop:trueな訳です。

  const params = {
    //Swiperの設定
    initialSlide: 0,
    spaceBetween: 10,
    slidesPerView: 1.25,
    pagination: true,
    centeredSlides: true,
    autoplay: {
      delay: 3000,
      disableOnInteraction: false,
    },
    loop: true,
  };

でもこれが中の要素を複製することで成り立っているみたいです……

私の対処としてはloopを外しました……この件の解消は手間すぎたので戦略的撤退です……
loopを外しても自動スクロールはちゃんと最初に戻るし、まぁいっかの精神です。個人開発ですから良いんです。

それでも若干不安定

イベント発火してから画像表示までに時間かかるのか、どうも微妙な挙動を繰り広げます……
原因模索中です。

placeholder="blur"も選択肢

これもドキュメントに書いてありました……
CSSで読み込み用のなにかを作るんじゃなくて、単純に読み込み時にふわっとさせたいだけならこれもありです。

こう書くと
https://github.com/vercel/next.js/blob/canary/examples/image-component/pages/shimmer.js
こういう感じのが
https://image-component.nextjs.gallery/placeholder
作れますよとのこと。ただしbase64エンコードしてねって感じらしい。

終わり

素人の私にはNext.js、ちょっとアレンジするだけでもめちゃくちゃ大変です……

Discussion

catnosecatnose

自分だったらそれぞれを画像コンポーネントとして切り出すと思います。で、コンポーネントの中で「読み込みが完了したかどうか」のステートを持たせるようにします。

ImageWithLoading.tsx
import { useState } from "react"

export const ImageWithLoading = ({ src }) => {
  const [loaded, setLoaded] = useState(false)

  return <Image
                 width={2000}
                 height={2000}
                 src={src}
                 objectFit="contain"
                 className={loaded ? "img-loaded" : "img-loading" } // 読み込み完了後はクラス名を変更
                 onLoadingComplete={() => setLoaded(true)}
               />
}

これをスライダーコンポーネントの中で読み込む感じです。

 <SwiperSlide key={key}>
  <ImageWithLoading src={src} />
</SwiperSlide>