⏱️

NextJSで魅せるロード画面にこだわってみる

に公開

はじめに

ことの始まりとしては、筆者が起業したので、電子広告台をケチるためにHPを作成する必要があった。
官報に記載すると数万円取られるからである。あとつまらない。

ぶっちゃけHPに書くことは公告義務を果たすための内容以外に特にないので、
演出をちょっとこだわることにした。

前提:Webサイトにおけるロード画面という概念について

基本的にロード画面というのは、ゲームの地形などを読み込む際に、ユーザーを待たせないために導入されがちだ。 だがHPやLPにおいては、必ずしもその限りではない。
動画サイトのように、大量のデータを扱う際にはロード画面は作成する必要があるだろう。
だが、殆どのウェブサイトは所詮HTMLなどのマークアップと、いくつかの画像データしか扱わない。
それも画像データは非同期でとってくること多いので、ロード画面を作る必要は殆どない。
だが、演出の一環として、あたかも存在する必要があるかのようにロード画面を表示するWebサイトは多いだろう。特に、ゲームやアニメのHPに多いように感じる。

そう。ウェブサイトのロード画面というのは、殆どが魅せるためのものなのだ。
問題は、システムとして機能美を持たせるためには、機能的には全く必要のないロード画面を、いかにも何かすごいことをしているかのように見せる必要があるのだ。

弊社のHPはどうするか

僕が考えついたのはUbuntuの起動画面だ。
会社のHPなんて今時個人が参照することはまずないだろうと思っている。
理由はシンプルで、主な使われ方は取引先がちゃんと存在する企業かどうか、ちゃんと特定商取引法に関する記述がどっかにあるか確認する程度だろう。
てなわけで、デザインには何かと理由をつけてこだわりたくない。その上で、内容がシンプルでも許されるのはどんなものか...と考えた時に、CLIをイメージしたロード画面を流せば、その後に出てくる情報がとんでもなくシンプルだったとしても、誰も文句を言わないはずである。

参考にした画面はこんな感じ、Ubuntuから引用

ただ、文字列だけだと味気がない。
arch linuxなど、起動時にロゴのAsciiアートを入れるものもあるので、それに倣ってロゴをくっつけよう。

てなわけでできたのがこれ 下に作り方をざっくり載せる
https://youtu.be/ZPuhi5AVVvg

まず、ロード画面に乗っける情報をあらかじめ定数で定義しておく
アスキーアートにエスケープ文字が使われているため、String.rawでエスケープしないようにする。
そして、アスキーアートをブートメッセージの中に配列として組み込むようにする。
順番を変えたくなった時にスムーズに変更できるから便利だ。

loading.tsx
const asciiArt = String.raw`

 $$$$$$\  $$\   $$\ $$$$$$$$\ $$\         $$\   $$$$$$$$\ $$$$$$$\  
$$$ __$$\ $$ |  $$ |\__$$  __|$$ |      $$$$ |  $$  _____|$$  __$$\ 
$$$$\ $$ |$$ |  $$ |   $$ |   $$ |      \_$$ |  $$ |      $$ |  $$ |
$$\$$\$$ |$$ |  $$ |   $$ |   $$ |        $$ |  $$$$$\    $$$$$$$  |
$$ \$$$$ |$$ |  $$ |   $$ |   $$ |        $$ |  $$  __|   $$  __$$< 
$$ |\$$$ |$$ |  $$ |   $$ |   $$ |        $$ |  $$ |      $$ |  $$ |
\$$$$$$  /\$$$$$$  |   $$ |   $$$$$$$$\ $$$$$$\ $$$$$$$$\ $$ |  $$ |
 \______/  \______/    \__|   \________|\______|\________|\__|  \__|
                                                                    
`;

const bootMessages = [
  asciiArt,
  "[ OK ] Starting system log daemon...",
  "[ OK ] Starting network manager...",
  "[ OK ] Starting Nginx web server...",
  "[ OK ] Starting firewall...",
  "[ OK ] Starting cron daemon...",
  "[ OK ] Starting system message bus...",
  "[ OK ] Starting hardware abstraction layer...",
  "[ OK ] Starting ACPI daemon...",
  "[ OK ] Starting Avahi mDNS/DNS-SD daemon...",
  "[ OK ] Starting CUPS printing system...",
  "[ OK ] Starting system logging service...",
  "[ OK ] Starting kernel log daemon...",
  "[ OK ] Starting system monitoring daemon...",
  "[ OK ] Starting disk management services...",
  "[ OK ] Starting network time synchronization...",
  "[ OK ] Starting package management services...",
  "[ OK ] Starting system update services...",
  "[ OK ] Starting backup services...",
  "[ OK ] System boot complete.",
  "[ OK ] Welcome to Our HP!"
];

そしてあとはこの文字を表示するロジックを組む
正規表現でまず[]の中にある要素はspanタグを適応して、色を変えるロジックを適応する。
また、等速で文字が出現していると、ロードしている という感じがしない。
そのため、遅延時間に乱数を与える。これによって、ユーザーが見慣れてしまうということを防ぐようにする。この「慣れ」対策は重要だ。
監査など特定の用事でHPを訪れている人は、何度もロード画面を見ることになるかもしれない。
演出の都合上ロード画面は数秒待たせることが殆どだが、全くおんなじ内容が流れてもその数秒は苦痛でしかないのだ。そのため、それをいくつかマシにするためにユーザーの潜在意識に、
「これは初めて見る画面だ!」と思い込ませる必要がある。

そのため、乱数を与えて文字の出現の速度を若干変える。
こんな程度で...と思うかもしれないが、トップ画面に遷移した後に、Rebootを何度か押してみてほしい。意外と効果を実感できるだろう。

loading.tsx
export default function Loading() {
  const [currentMessageIndex, setCurrentMessageIndex] = useState(0);

  useEffect(() => {
    if (currentMessageIndex < bootMessages.length - 1) {
      // ASCIIアートの場合は少し長めの遅延
      const delay = currentMessageIndex === 0 ? 500 : Math.random() * 150 ; 
      const timer = setTimeout(() => {
        setCurrentMessageIndex(currentMessageIndex + 1);
      }, delay);
      return () => clearTimeout(timer);
    }
  }, [currentMessageIndex]);

  return (
    <div className="flex flex-col items-start h-screen bg-black text-white p-4 text-xl">
      {bootMessages.map((message, index) => {
        // ASCIIアートの場合は特別な処理
        if (message === asciiArt) {
          return (
            <motion.div
              key={index}
              initial={{ opacity: 0 }}
              animate={{ opacity: index <= currentMessageIndex ? 1 : 0 }}
              transition={{ duration: 0.1 }}
              className="text-lime-500 font-mono whitespace-pre"
            >
              {message}
            </motion.div>
          );
        }
        
        // 通常のブートメッセージの処理
        const match = message.match(/^\[(.*?)\]\s(.*)/);
        const status = match ? match[1] : '';
        const rest = match ? match[2] : message;
        return (
          <motion.div
            key={index}
            initial={{ opacity: 0 }}
            animate={{ opacity: index <= currentMessageIndex ? 1 : 0 }}
            transition={{ duration: 0.1 }}
            className="text-lg font-mono"
          >
            <span>[</span>
            <span className="text-green-500">{status}</span>
            <span>] {rest}</span>
          </motion.div>
        );
      })}
    </div>
  );
}

トップ画面で強制的にロード画面を表示する。

loading.tsxという名前でファイルを作成すると、ページロード時に強制的に出現するようになるが、ロードが終わると通常であれば強制的に終了されてしまう。 まあ当たり前だが。

それを避けるために、直下にアクセスした時のみLoadingを強制的に読み込むようにする。
setTimeoutをuseEffect内で利用して、3秒経ったらロードを解除するようにする。

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

  useEffect(() => {
    const timer = setTimeout(() => {
      setIsLoading(false);
    }, 3000);

    return () => clearTimeout(timer);
  }, []);

  if (isLoading) {
    return <Loading />;
  }

完成!

実際のHPは君の目で確かめろ!
見ての通りロード画面以外はすっからかんだ!
https://www.0utl1er.tech

Discussion