🫠

ウェブサイトのパフォーマンス改善をしよう

2023/11/27に公開

NOT A HOTELでのウェブ開発というとスマホを使用した無人チェックインからの宿泊体験が思い浮かびそうですが、ウェブ上で別荘を購入するためのサイトの運営もしています。最近では、インスタグラムやタクシー広告を見る方もいるのではないでしょうか?

今回は、ショップサイト、広告から遷移するLPサイトのパフォーマンス改善をどのように行ったかを書いていきます。

まずは計測

計測するページ

現在販売中の水上TOJIのLPを元に計測を行いました。
日々改善しているので、今見ると状況が違いますが、八枚の画像を自動でスライドするファーストビューで、スクロールすると、たくさんの画像が表示されています。
https://notahotel.com/lp/toji

技術としては、Firebase HostingでのNext.jsでの静的ビルドと、Cloudflare Polishを使用した画像サイズの最適化をしています。

計測結果

問題を把握するために、WebPageTestを使って、計測しました。
テスト環境は、そこまで潤沢なネットワーク環境ではない、iPhoneX Safari, Osaka, Cableで計測しています。


気になるところは、以下でしょうか

  • ファーストビューの画像が表示されるのに、28秒かかっている
  • (切れてしまっているが)データの読み込みサイズが、15MBもある
  • 初期表示に不要な画像が先に読み込まれている
    (この記事を書いていて気づきましたが、PC用の画像が読み込まれていますね・・・)

改善指標を決める

今回は、LPであり、クリックレートの改善に貢献するために、FVの画像を如何に早く表示させるかを軸に改善を計りました。

原因の分析

もう少し詳しく初回の画像表示を遅らせる原因を探ってみましょう。

  • 画像のファイルサイズが、1MBオーバーと大きい
  • Cloudflare Polishを使用しているが、それでも大きい
  • 画像スライドにSwiperを使用しているが、画像表示がjsファイルに依存するため遅い。画像ファイルのURLをhtmlファイルに埋め込みたい。
  • 比較的小さなsvgファイルの取得にリクエストを割かれている

よくある遅延読み込みの対応に関しては、2点問題がありました。

  • lazyloadを設定していない
  • もうひとつ、Swiperがpictureタグに囲まれていないimgタグの画像を先読みしている(以下が問題のコード)
export default function loadImage(imageEl, src, srcset, sizes, checkForComplete, callback) {
  var window = getWindow();
  var image;

  function onReady() {
    if (callback) callback();
  }

  var isPicture = $(imageEl).parent('picture')[0];

  if (!isPicture && (!imageEl.complete || !checkForComplete)) {
    if (src) {
      image = new window.Image();
      image.onload = onReady;
      image.onerror = onReady;

      if (sizes) {
        image.sizes = sizes;
      }

      if (srcset) {
        image.srcset = srcset;
      }

      if (src) {
        image.src = src;
      }
    } else {
      onReady();
    }
  } else {
    // image already loaded...
    onReady();
  }
}

大きな問題としては、サイト上の見出しのMB101 BOLDの読み込みにTypeSquareを使用しているのですが、このフォントの読み込みで、auto_load_font=trueを使うと、ウインドウ読み込み完了まで、bodyに対して、opacity: 0が設定されていました。

つまりは、jsが展開され、画像やリソースが全て読み込まれるまで、透明度100%、画面が白いままになっていることで、初回の画像表示が28秒かかっていました。

検証時には使われていないが、映像を流していた時もあり、その場合、もっと時間がかかっていました。

改善をする

TypeSquare

TypeSquareでのフォントの自動読み込みをやめ、ページ表示時に自前で読み込むようにしました。

useEffect(() => {
  if (window.Ts) {
    window.Ts.loadFont()
  }
}, [])

これだけで見違えるように早くなりました。
画像を表示するための指標もonloadから、LCPになりました。

Swiperの置き換え

指標がLCPになったので、jsの処理を挟まずに画像が表示されるようにします。
自前実装に変更しました。

const ImageGenerator = (
  duration: number,
  pcImages: string[],
  spImages?: string[]
) => {
  const size = pcImages.length
  const basePercent = 100 / size

  return styled.div`
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-repeat: no-repeat;
    background-position: center center;
    background-size: cover;
    opacity: 0;
    animation-name: ${AnimationGenerator(basePercent, 40)};
    animation-duration: ${size * duration}s;
    animation-iteration-count: infinite;
    ${GetAnimations(duration, pcImages, spImages ?? pcImages)}

    @media ${mediaQuery.laptop} {
      animation-name: ${AnimationGenerator(basePercent, 70)};
    }
  `
}

const AnimationGenerator = (basePercent: number, startXPosition: number) => {
  return keyframes`
    0%   { opacity: 0; transform: translateX(0px); }
    ${basePercent * 0.75 + '%'}  { opacity: 1; }
    ${basePercent * 1.25 + '%'}  { opacity: 1; }
    ${basePercent * 1.75 + '%'}  { opacity: 0; }
    100% { opacity: 0; transform: translateX(${-startXPosition}px); }
  `
}

const GetAnimations = (
  duration: number,
  pcImages: string[],
  spImages: string[]
) => {
  let str = ''
  for (let index = 0; index < pcImages.length; index += 1) {
    str += AnimationDelay(index, duration, pcImages[index], spImages[index])
  }
  return str
}

const AnimationDelay = (
  index: number,
  duration: number,
  pcImage: string,
  spImage: string
) => {
  return `
    &:nth-child(${index + 1}) {
      animation-delay: ${`${index * duration}s`};
      background-image: url(${pcImage});
      @media ${mediaQuery.mobile} {
        background-image: url(${spImage});
      }
    };
  `
}

background-imageを使って、読み込み途中の画像を表示しないようにしています。(その代わりに、fetch-priorityで一枚目を別で優先読み込みさせています)

Cloudflare Imagesへの移行

次に画像ファイル自体の読み込みを早くするための対策をします。
ファイルサイズが、Cloudflare Imagesのほうが軽くなるので、移行しました。
ファイルパスをIDにする方法がわからず苦労しましたが、なんてことはないidに指定すれば良かった・・・。
https://developers.cloudflare.com/images/cloudflare-images/upload-images/custom-id/
https://developers.cloudflare.com/api/operations/cloudflare-images-upload-an-image-via-url

$ filepath=sample/example.png
$ curl -X POST -H "Authorization: Bearer ${TOKEN}" \
"https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/images/v1" \
--form file=@./$filepath \
--form "id=${filepath}"

その他

画像表示のための対策は、以上になります。
あとは、よくあるloading="lazy"や、Vimeoや映像ファイルの読み込みタイミングをコントロールし、初回のデータ量を削減しました。

改善結果

上から5番目にFVの画像をダウンロードしているのがわかると思います


初回のFV画像の表示は、28秒から1.7秒。
データサイズは、15MBから3MBになりました。
LCP自体は、1.2秒から0.8秒と、計測結果としては、怪しそうではある。

CrUX Dashboardでも、LCPが半分になっています(WebPageTestの結果をふまえると恐ろしい)。

NOT A HOTELに関わるクリエイターや建築チームが破茶滅茶にこだわった空間の写真やCGパースを、如何に早く綺麗に伝えるか、日々奮闘しております。

NOT A HOTEL

Discussion