ローディングとファーストビューのアニメーション

表示に3秒以上かかるページは半数近くの人が離脱するようです

ローディングアニメーションを実装して待つストレスを軽減させたり
導線用のファーストビューを準備してページを良くみせるたり
工夫が必要かも😅

目次
  1. pace-jsでプログレスバーを実装
  2. ローダーの実装(基本)
  3. imagesLoaded(画像の読み込みを監視)
  4. ファーストビューでアニメーション
  5. 予備知識

pace-jsでプログレスバーを実装

「pace-js」はレイアウトも準備されていてダウンロードするだけで簡単にプログレスバーが実装できます

CDNの場合<head>内に追加
設置したいデザインのCSSファイルをダウンロードして読み込みます

<head>
<script src="https://cdn.jsdelivr.net/npm/pace-js@latest/pace.min.js"></script>
<link rel="stylesheet" href="loading-bar.css">
...
</head>
<body>
 <div class="content">
     <!-- コンテンツ部分 -->
 </div>
</body>

.pace-runningは読み込み中につくクラス
.pace-doneは読み込み完了後につくクラス

.content{
   opacity:1;
   visibility: visible;
}

.pace-running .content{
    opacity:0;
    visibility: hidden;
}
 /*または*/
.content{
    opacity:0;
    visibility: hidden;
}
.pace-done .content{
   opacity:1;
   visibility: visible;
}

ローダーの実装(基本)

ローディング画面とコンテンツを重ねる
loadイベント(画像やCSS等の読み込みが完了)でローディング画面を非表示にします

ローディングアイコンはこちらのサイトからコピペさせていただきました♪
https://dev.to/afif/i-made-100-css-loaders-for-your-next-project-4eje
ちなみに読み込みファイルが重くうまくアニメーションできないときは
ローディングアイコンは高さなどを計算していないものにするか
display:noneでコンテンツを非表示にして読み込み完了後に表示する

<body>
<div class="loader">
  <div class="classic-1"></div>
</div>
<!-- コンテンツ部分が始まる -->
<div class="content">

</div>
</body>
.loader {
  position: fixed;
  top:0;
  left: 0;
  width:100%;
  height:100vh;
  transition: all 1s;
  background-color: #ccc;
  display: flex;
  align-items: center;
  justify-content: center;
 }
 /*ローディングアイコンのコピペ*/
.classic-1:before {
  content:"Loading...";
}
.classic-1 {
  font-weight: bold;
  font-family: sans-serif;
  font-size: 30px;
  animation:c1 1s linear infinite alternate;
}
@keyframes c1 {to{opacity: 0}}

 /*ローディング画面を非表示*/
.loader.hidden{
   opacity:0;
   visibility: hidden;
}
 //ページを読み込んだらhiddenクラスをつけててローダーを非表示にします
window.addEventListener("load", ()=>{
   document.querySelector('.loader').classList.add("hidden")
 })

imagesLoaded(画像の読み込みを監視)

imagesLoadedで画像の読み込み状況を監視できます

GSAPを利用して読み込み状況のプログレスバーを表示します

<head>
<!-- imagesLoaded CDN -->
<script src="https://unpkg.com/imagesloaded@5/imagesloaded.pkgd.min.js"></script>
<style>
.loader{
    position: fixed;
    background:#ccc;
    top: 0;
    left: 0;
    width: 100%;
    height: 100vh;
    overflow: hidden;
}
.progress {
    background-color: blue;
    position:absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 50vw;
    height: 2px;
}
</style>
 <!-- 省略 -->
</head>
<body>
  <div class="loader">
    <div class="progress"></div>
  </div>
<!-- ここからコンテンツ部分 -->
<div class="content">
  <!-- コンテンツ -->
</div>
<!-- GSAP CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.3/gsap.min.js"></script>
<script src="main.js"></script>
</body>
let loadedImageCount = 0; //ロードした画像数
let imageCount;
// Imagesloadedのセットアップ content内の画像を監視
const imgLoad = imagesLoaded(".content");
//画像の総数を取得
imageCount = imgLoad.images.length;
//左からX方向へのTween
const progressTween = gsap.from(".progress", {
  paused: true, //アニメーションを一時停止
  scaleX: 0,
  ease: "none",
  transformOrigin: "left",
});
//progressTweenを更新
function updateProgress(value) {
  gsap.from(progressTween, {
    progress: value / imageCount, //進捗状況
    duration: 0.3
  });
}
// 画像がロードされたら
imgLoad.on("progress", function () {
  loadedImageCount++; //ロードした画像数をカウント
  updateProgress(loadedImageCount); //ロードした画像数を引数にupdateProgressを実行
});

// 画像がロードが完了したら
imgLoad.on( 'done', function() {
    gsap.set([".progress", ".loader"], { autoAlpha: 0 });
});

ファーストビューでアニメーション

ファーストビューを表示している間に外部ファイルの読み込みを完了させます

ただし、SNSにリンクを貼る場合はロードが完了してからアニメーションを実行した方が良い
理由は、SNS内部ブラウザは、通常のブラウザとは異なる動作をする場合があります。これがJavaScriptの実行タイミングに影響して、アニメーションがうまく動作しない可能性があります

GSAPを使って実装してみる
最初、コンテンツ部分はdisplay: none;で非表示に
コンテンツ部分の上に.loaderと.loader-contentがのる
1つ目のタイムラインと2つ目のタイムラインをつなぐタイムラインを作成
1つ目のタイムラインを実行
すべて読み込みが完了後に2つ目のタイムラインを実行

<body class="is-loading">
  <div class="loader">
   <div class="mask">
       <div class="loader-inner"></div>
    </div>
  </div>
  <div class="loader-content">
    <div class="loader-content-inner">
        <div class="loader-title">
            <div class="title-mask"><span>Merry</span></div>
            <div class="title-mask"><span>Christmas</span></div>
         </div>
         <div class="loader-image">
            <div class="image-mask">
                <img loading="eager" class="image-item" src="./images/img15.jpg" />
             </div>
         </div>
     </div>
  </div>
<!-- ここからコンテンツ部分 -->
<div class="content">
  <!-- コンテンツ -->
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.3/gsap.min.js"></script>
<script src="main.js"></script>
</body>
*{ margin:0; padding:0;}
:root {
  --loader-width: 80vw;
  --loader-height: 80vh;
}
.is-loading .content{
    display:none
}
.loader, .loader-content {
  visibility: hidden;
  position: fixed;
  z-index: 1;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
}
.loader-content {
  z-index: 2;
  display: flex;
  justify-content: center;
  align-items: center;
  background: transparent;
}
.mask {
  position: absolute;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  background: #c1d1e0;
}
.loader-inner, .loader-content-inner {
  width: var(--loader-width);
  height: var(--loader-height);
}
.loader-inner {
  background: #bfc9bd;
}
.loader-title {
  position: absolute;  
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  font-size: clamp(3.25rem, 8vw, 6.25rem);
  line-height: clamp(3.25rem, 8vw, 6.25rem);
  text-align: center;
  font-family: serif;
  color: #fff;
}
.title-mask {
  overflow: hidden;
}
.title-mask span {
  display: block;
}
.loader-image {
  overflow: hidden;
  position: relative;
  width: var(--loader-width);
  padding-bottom: var(--loader-height);
}
.image-mask {
  overflow: hidden;
  position: absolute;
  width: 100%;
  height: 100%;
}
.loader-image img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  opacity: 0.8;
}
//InとOutのタイムラインをつなぐタイムライン
const tlLoader = gsap.timeline();

//1つ目:Inのタイムライン
const tlLoaderIn = gsap.timeline({
    defaults: {
      duration: 1.5,
      ease: "Power2.out",
    },
//Inが終わったタイミングでdisplay: none;を外す
    onComplete: () =>
      document.querySelector("body").classList.remove("is-loading"),
  });
  tlLoaderIn
//残像があるのでcssでvisibility:hidden;にしている
    .set([".loader", ".loader-content"], { autoAlpha: 1 })
    .from(".loader-inner",{
        scaleY: 0,
        transformOrigin: "bottom",
      },0.2)
    .addLabel("image") //ラベルは写真とマスクを同じタイミングでずらすため(視差効果用)
    .from( ".image-mask", {
        yPercent: 100,
      }, "image-=0.8")
    .from( ".image-item", {
        yPercent: -80, //ずらす
      }, "image-=0.8" )
    .from( ".title-mask span", {
        yPercent: 100,
        stagger: 0.5,
      }, "image-=0.5");
//Inのタイムラインを実行
tlLoader.add(tlLoaderIn)

//2つ目は全ての読み込み完了後に
window.addEventListener('load', () => {
 const tlLoaderOut = gsap.timeline({
   defaults: {
     duration: 1.5,
     ease: "Power2.inOut",
   }
 });
 tlLoaderOut
   .to([".title-mask", ".title-mask span"], {
       yPercent: -500,
       stagger: 0.5,
     },0 )
   .from(".content", {
       y: 100,
     },0 )
   .to( [".loader", ".loader-content"],{
       yPercent: -100,
     },0 )

//Outタイムラインを実行
 tlLoader.add(tlLoaderOut);
})

予備知識

ブラウザがレンダリングする準備

  • HTMLをダウンロード → HTMLの解析(パース)→ DOMツリーの構築
    *HTMLの解析とDOMツリーの構築: ブラウザはHTMLドキュメントを読み込み、それを解析してDOMツリーを構築します
  • CSSの解析(パース) → CSSOMの構築
    *同時に、CSSファイル(内部のものも含む)は解析され、CSSOMが構築されますCSSOMはスタイルの情報を含む構造で、DOM要素の見た目を定義します
  • DOMとCSSOMが完了 → レンダーツリーの構築 → レイアウトの計算
    ポイントdisplay: none;はレンダーツリーに登録されません
    レンダーツリーに登録したい場合はvisibility: hiddenを使用
  • HTMLの解析の途中「scriptタグ」を見つけたらJavaScriptファイルをダウンロード → コンパイル(機械語に変換)→ JavaScript実行

ここまではメインスレッドで実行されます(JavaScriptの実行を含む)
レイアウトの計算がされた後、ピクセルへの描画(またはレンダリング)がGPUによって行われる(メインスレッドの負担が軽減)
*更新があった場合、再計算(不要な工程は省略)されます

HTMLの解析(パース)
上から順番に外部ファイルを読み込む箇所に到着すると解析を一時停止(備考参照)して外部ファイル(画像やJS)のダウンロードと処理の実行

JavaScriptのダウンロードや実行はレンダリングをブロックするので基本的にはbodyの下部で読み込む(注意あり)
*bodyの下部での読み込みや defer属性を使用すればDOMContentLoaded イベントは必要はありません
*レンダリング前にJavaScriptを実行したい場合はhead内におく
*JavaScript がたくさんある大規模なサイトではbodyの下部での読み込みはサイトを遅くするのでhead内でasyncとdeferを使用します

CSSを下部で読み込むと、レイアウトの適用が遅れ、体験が低下する可能性があります
CSSの取得はHTMLのダウンロードや解析をブロックしませんがJavaScriptがDOM要素を操作する場合、そのDOM要素のスタイル情報が必要になるため、CSSが完全に読み込まれて解析されるまで、依存するJavaScriptの実行がブロックされることがあります

備考

scriptタグ

  • deferもasyncも付けない
    HTML解析中にscriptタグがあると解析を一時停止
    JSファイルのダウンロードと実行
  • async
    JSダウンロードは別のスレッドで実行
    依存関係がない場合や早く実行したい場合に使用
    ダウンロードが完了すると、スクリプトが実行され、ページのレンダリングがブロックされます
    実行順序は保証されません
    *スクリプトの実行順序が重要でない場合に適しています
  • defer
    JSダウンロードは別のスレッドで実行
    DOMに依存する場合や依存関係がある場合に使用
    ページのコンテンツがすべて読み込まれるまで実行されません
    scriptタグを書いた順に実行される

JavaScriptのイベント

  • DOMContentLoadedイベント:DOMの読み込みが完了したら発火
  • loadイベント:画像やCSS等の外部ファイルもすべて読み込みが完了してから発火

「画像やiframe」のloading属性

  • auto: デフォルト値・設定しないのと同じ
  • eager:リソースをすぐに読み込む
  • lazy:ビューポートに表示されたらリソースを読み込む(遅延読み込み)

一般的には
ファーストビューより下のリソース(3000px下側あたり)はloading="lazy"
ファーストビューのリソースはloading="eager"

画像はサイズを指定をします(imgタグにwidthとheight画像にサイズを設定、CSSでheight: auto;)
*指定しないとブラウザが画像のサイズを知るためにロードが完了するまで画像のスペースはゼロになります

アニメーションに適したプロパティはレンダリングの計算量が少ないopacitytransformです
*opacityと3Dtransformはメインスレッドの外で、コンポジタースレッドとGPUを使用して処理されます

レイヤーという概念を用いて対象要素だけ再レンダリングします
レイヤーを生成するプロパティ:opacity・3Dtransform・will-change
レイヤーを生成する:video・canvas

3Dtransform
transform:translateZ(0);をつけてブラウザをだます
*transformもSafariが使用しているWebkit以外はレイヤーで処理されるそうなので、Safariでチラついたときのハックとして使用されたりします

will-changeプロパティの使用は要素にどのような変更を加えるかを前もってブラウザに知らせることで最適化する方法です
例えば、transformの処理を先にブラウザに知らせたい時はwill-change: transform;

レイヤーはパフォーマンスを改善しますが、メモリー管理の面ではコストのかかる処理なので濫用するべきではありません

  • スタイルシートでは控えめに
    JavaScriptを使用して要素がviewportの見える位置にある時にwill-changeを設定、不要になれば削除など
  • 変化が生じる直前の要素にwill-changeを設定しても効果はない