📸

写真サークルのWebページを作成した話

2021/06/18に公開

はじめに

今回、私が所属する写真サークル 珪素光画のWebページを作成しました。
https://keiso-kouga.netlify.app/

こちらの製作に当たっての道のりや苦労した点などをこの記事に記載していこうと思います。

技術選定

今回は以下のようなサービスやOSSを利用して実装をしました。

  • React - みんな大好き
  • Netlify - なるべくお金をかけずに済みました
  • ant design - シンプルなUIコンポーネントやレイアウトを提供しています
  • Swiper - 写真の作例を表示するのに活用しました

パフォーマンス向上のためにやったこと

写真サークルというだけあって、Webページのコンテンツで「写真」がかなり重要になります。
主に以下のような場面で写真を利用しています。

  • Webページの見出しに大きく1枚
  • サークルの雰囲気を伝えるためのギャラリーに3枚
  • メンバー紹介1人につき作例3枚

今はサークルのメンバーが僕を含めて2人なので、メンバーの作例写真はまだ少ない方ですが、
珪素光画が大人気コンテンツとなりメンバーが増えたら、それはもう見るに耐えないページになってしまします。
この文章を書いた直後に新規メンバーが2人増えました。やったね!たえちゃん!

1. 基本はwebpで表示

一眼で撮ったバカデカjpeg画像をそのまま<img>で表示すると爆死してしまうので、
Googleが提供しているwebp拡張子に変換して利用します。
公式Docによると、pngやjpeg画像より25%程度軽量化されており、サイズもかなり軽量になっています。

webpは全ての環境で表示が出来るわけではないので、<picture>タグなどを用いて表示の切り替えをしてあげる必要があります。

<picture>
  <source type="image/webp" srcSet="path/to/webp/image">
  <img src="path/to/jpeg/image" alt="icon" />
</picture>

ちなみに2021/05現在ではiOSやSafariでもwebpの対応も始まっているようです。↓↓↓
https://en.wikipedia.org/wiki/WebP#Support

webpへの変換は、CLIツールが提供されているので、とても楽に変換が出来ました✨

$ brew install webp
$ cwebp test.jpeg -o test.webp

ちなみに、このWebサイトで導入しているant designのImageコンポーネントではfallbackプロパティが付与されています。
これは本来、画像の読み込みに失敗した場合に表示する画像を設定しますが、今回はwebp画像を読み込めなかった場合を想定して、fallbackプロパティにjpeg画像をセットしています。

<Image
  src={path to webp image}
  fallback={path to jpeg image}
/>

2. ロード画面の作成

webpで対応をするとは言ったものの、環境によっては表示まで時間がかかることがあります。
特に今回、Webページのトップには大きな一枚の写真が配置されています。↓↓↓

Webサイトに初めて訪れた人が見る最初の写真であり、一番の見せ所です。
そのため、なるべく大きく高画質で表示したいところ。ですがそれを愚直に表現すると読み込みまで時間がかかり、かなり寒い感じになります。

最低限、タイトルの画像の読み込みが終わるまでは他のテキストなどの要素は表示したくないので、ロード画面を作ることにしました。

画像をPreloadする処理を走らせ、それらをPromiseで包括することで読み込みの完了をstateで管理することが可能になります。

Image.tsx
const [isLoading, setIsLoading] = useState(true);

const fetchImage = new Promise((resolve, reject) => {
    // 画像のPreloadを実行
    const img = new Image();
    img.src = imagePath;
    img.onload = () => {
        resolve("completed to load");
    };
});

useEffect(() => {
    // 画像の読み込みが完了した時点でstateを更新する
    fetchImage.then((successMessage) => {
        setIsLoading(false);
    })
});

あとはComponent内でstateの値に合わせてロード画面との出し分けをします。

Title.tsx
return (
  <>
  {isLoading
  ?
  <div className="title-cover">
    // ロード画面
  </div>
  :
  <div className="title">
    // タイトルやら何やら
  </>
);

ちなみに、ロード画面はant designのSpinコンポーネントを利用しました。

3. プレビュー画像とサムネイル画像を分ける

今回作成したWebページは全体で1ページしか無く、ユーザがサイトを訪れた時点で全ての画像が読み込まれます。
そのため、下の画像のように500KB前後の画像が複数枚DLされます。
webpへの変換を済ませたり、Preloadを実装していたとしても初っ端から大きい画像を何枚も取得するのはしんどいでしょう。(特にwebpに対応していない一部バージョンのiOSやmacOSは地獄です。)
終いにはメンバーが増えまくってしまうと尚のこと。ここは少しでも1枚あたりの画像サイズを小さくしたいところ。

そこで今回は、拡大プレビュー用サムネイル用の2枚を用意してあげることにしました。
antdのImageコンポーネントにはpreviewプロパティが用意されているので、ここにサイズが大きい画像のパスを指定してあげます。

Image.tsx
<Image
  src="サムネイル用のサイズが小さい画像"
  preview={{
    src: "拡大プレビュー用のサイズが大きい画像"
  }}
/>

こうすることで、Webページをロードした段階ではサムネイル用の小さい画像が読み込まれ、各画像をタップしてプレビューを表示する動作をした時だけ、その画像の拡大プレビュー用の大きいサイズが読み込まれます。
今回用意したサムネイル画像は、拡大用のプレビュー画像のおよそ半分のサイズにしました。
塵積ですが、今後増えるかも知れないコンテンツなので外せないところです。

4. メンバー追加や同人誌の新規発行に強く

有り難いことに、当サークルのメンバーが増える機会がありました。
また、同人誌も今後新しく刊行するかも?みたいな話が内輪で上がっており、活動としては大変盛り上がっております。

そうなるとWebページも随時更新する必要があり、恐らく一番変更が加えられるコンポーネントになるでしょう。
その度にtsxファイルを変更するのは気が引けますし、「やっぱりメンバー紹介のレイアウト変えたい」ってなった時は地獄です。
そこで、メンバーや同人誌などのデータのみを格納するtsファイルを作成して、ルートからpropsでバケツリレー的にデータを渡す構成で実装をしました。

data.ts
// メンバーの情報
export const members = [
    {
        id: nanoid(),
        name: "Aさん",
        icon: "",
        photo: "",
	twitter_url: ""
    },
    {
        id: nanoid(),
        name: "Bさん",
        icon: "",
        photo: "",
	twitter_url: ""
    },
    ...
]

これをApp.tsxなどでimportして、該当するComponentに渡してあげて、内部でループを回してDOMを描画します。

Member.tsx

function Members(props: Props) {
  const { members } = props;
  return (
    <div className="member-cell">
      {members.map((member) => {
        const { name, icon, photo, twitter_url } = member;
        return (
          // アイコンの配置とか諸々
	  // メンバーが増えても、この中身は変わらない
        );
      })};
    </div>
  );
}
data.ts
export const members = [
    {
        id: nanoid(),
        name: "Aさん",
        icon: "",
        photo: "",
	twitter_url: ""
    },
    {
        id: nanoid(),
        name: "Bさん",
        icon: "",
        photo: "",
	twitter_url: ""
    },
    {
	// メンバーが増えたらdataを追記するだけでおk
        id: nanoid(),
        name: "Cさん",
        icon: "",
        photo: "",
	twitter_url: ""
    },
    ...
]

これをすることで、メンバーの追加や削除あるいは同人誌の新規発行などの時はdata.tsを編集するだけで完結します。

さいごに

Webページの見た目自体はかなりシンプルなもので、そこまで手の込んだことはしていませんが
写真をメインとするWebページというだけあって、画像の描画や読み込みなどにかなり頭を使った実装になりました💮
是非一度、珪素光画のWebページに遊びに来ていただけたらなと思います!
https://keiso-kouga.netlify.app/

Discussion