ABOUT THIS SITE

派手な演出を持ちながら速度を落とさないこと自体が、このサイトの主張です。 実装は変わり続けるため、現在の構成と変えた理由を、数値と一緒に公開します。

実測値

最終計測: 2026/09/13 14:31 / Lighthouse (mobile, simulated throttling, gzip配信)

  • 100Performance目標 95 以上
  • 100Accessibility目標 100
  • 100Best Practices目標 100(警告)
  • 100SEO目標 100(警告)
パフォーマンス予算の実測
指標実測目標
LCP1.73s2.0s 以下
CLS0.0000.05 以下
TBT0ms200ms 以下
初期JS (gzip)5.4KB30KB 以下(設計)
フォント合計162.1KB200KB 以下

これらの数値は手打ちではありません。PRでは Lighthouse で予算を検証し、 main が更新されたときにトップページを再計測してsrc/data/metrics.jsonへ保存します。このページが表示するのは、その最新値です。 Performance・Accessibility・LCP・CLS・TBT・転送量はマージを止める基準、 Best Practices と SEO は警告として扱います。

このページだけ LCP の目標を 2.4s に緩めています。当初これを「和文フォントの到着待ち」と説明していましたが、誤りでした。 FCP が 1.9s あり、LCP との差は 0.2s しかありません。効いているのはHTML 文書そのものの大きさで、文章を足すとそのぶん素直に遅くなります。 preload もサブセットの切り分けも、文書量のボトルネックには効きませんでした。 内容を削れば通りますが、それはこのページの存在価値を削ることになるので、 数値のほうを譲りました。CI ではこのページだけ 2.4s、他は 2.0s のまま判定しています。

技術スタック

何を使ったかは package.json を見れば分かります。 ここに書くのは、なぜそれを選んだかです。

フレームワーク

Astro
静的HTMLを先に出し、必要な演出だけをクライアント側へ載せる。作品と意思決定ログは Markdown の Content Collections として管理し、ビルド時に検証している。
TypeScript (strict)
演出の中身まで型を通す。React / Vue などのUIランタイムは加えず、Astro コンポーネントと素の TypeScript で実装している。

見た目

素のCSS + カスケードレイヤー
Tailwind は使わない。デザイントークンを一次情報として持ちたかったのと、ユーティリティクラスの羅列ではこのページで説明しづらいため。
Archivo / Zen Kaku Gothic New / IBM Plex Mono
全てセルフホスト。Google Fonts への実行時接続はしない(third-party接続はLCPを悪化させる)。

演出

GSAP + ScrollTrigger
看板の点灯シーケンスと縦看板のパララックスに限定して使う。最初の操作か2.5秒経過まで取得を待ち、reduced-motion では取得しない。
OGL
背景シェーダー1枚に必要な処理だけを扱える薄い WebGL ラッパー。GPUを使えない環境では初期化せず、CSSの背景を残す。

配信

Cloudflare Workers (Static Assets)
Worker スクリプトを持たないアセットのみの構成。公開URLは mawo.dev に一本化し、PR用のプレビューURLだけを残している。
GitHub Actions + Lighthouse CI
PRでは4ページを計測して予算を検証する。main 更新後はトップを再計測し、このページが読む実測値とフォントのサブセットを自動更新する。

こだわりと工夫

現在の実装へ至るまでに実際に踏んだ問題と、その対処です。一般論ではなく、このサイトで測って決めたことだけを載せています。

  1. 01

    和文フォントを出力HTMLから逆算して切る

    サブセット対象の文字を、ソースコードではなくビルド後のHTMLから集めている。ソースを走査すると日本語のコード内コメントまで拾い、描画されない文字のためにフォントが太る。Bold も本文全体ではなく、見出し・strong・表見出しに実際に出る文字だけから毎回作り直す。

  2. 02

    和文をトップ用と残りに分割する

    サイト全体の和文を1ファイルにすると、入口ページが「このサイトについて」の長文で使う文字まで読み込む。unicode-range で core / ext に分け、トップと成果物一覧は core だけで完結させた。このページの導入文だけは明示的に core へ加える。境界を広げると入口のLCPが悪化するため、実測で範囲を決めている。

  3. 03

    欧文は1ウェイトに固定する

    可変フォントは便利だが、LCP要素の書体では話が別だった。wght 400-700 の可変で20.1KB、ウェイト別2ファイルではリクエストが1本増える。どちらもLCPを1.86s→2.10sまで悪化させたので、wdth 80 / wght 700 の単一インスタンス(9.3KB)に固定した。

  4. 04

    preload はページのLCP要素に絞る

    書体を全部 preload すると帯域を奪い合って逆効果になる。共通では欧文見出し用の Archivo だけを先読みし、和文の導入文がLCPになるこのページだけ Zen Kaku Gothic New Regular を加えている。IBM Plex Mono は先読みしない。

  5. 05

    アイランドの起動スクリプトを1本にまとめる

    コンポーネントごとに script を置くと、小さなチャンクが別々のリクエストになる。成果物一覧では看板の点灯・光害の空・縦看板を1本の入口から起動し、GSAP と OGL は必要になった時点で動的 import する。さらに最初の操作か2.5秒経過まで観測自体を待ち、本文とフォントを先に配る。

  6. 06

    GPUが無い環境では背景シェーダーを動かさない

    GPUを持たない環境ではWebGLがソフトウェア描画になり、全画面のfBmシェーダーを毎フレームCPUで解くことになる。実際CIで Performance 0.71 / TBT 2180ms まで落ちた。内訳を見るとスクリプト実行は290msで、残りはラスタライズだった。描画元を検出して初期化を諦め、CSSのグラデーションに任せている。

  7. 07

    計測は本番と同じ圧縮条件で行う

    astro preview は無圧縮で配信するため、そのまま測ると本番より悪い数値が出る。Workers Static Assets の圧縮あり配信に合わせ、計測用に gzip を返す静的サーバーを用意した。ローカルとCIで同じ条件を使う。

  8. 08

    窓グリッドをやめて看板にした

    当初は3:4の窓を敷き詰め、65〜75%を装飾の消灯セルで埋めていた。作品名とサムネイルはホバー時のみ表示する設計だった。これが機能しておらず、静止状態では暗い四角が並ぶだけでどれが作品か分からない上、タッチ端末にはホバーが無いためタップするまで内容が不明だった。装飾セルは「空枠」に見えるだけで装飾として働いていなかった。比喩より機能を優先し、1作品=1枚の看板として名前と姿を常時見せる形に変えた。夜の街で実際に目に入るのは窓ではなく看板で、看板はそもそも読ませるために光っている。

  9. 09

    ネオンの芯は白に寄せる

    実物のネオン管は中心が白飛びしていて、色は周囲に出る。中心まで管の色のままだと「文字に影を付けただけ」に見える。芯・内側グロー・外側グロー・壁への漏れ光の4層で組んでいる。

  10. 10

    光の上でもコントラストを実測する

    グローは背景を明るくするので、文字色との比が下がる。写真背景を入れたとき、ヘッダーのナビが青いネオンの上に来て 3.30:1 まで落ちていた(基準は4.5:1)。文字を消した状態でスクリーンショットを撮り、文字が乗る領域の最も明るい画素と比較して確認している。

検索結果での見え方

公開当初、このドメイン名は GitHub のリポジトリ名と同じでした。検索すると、 サイトより先にソースコードのリポジトリが出てきます。検索エンジンから見えるのは同じ文字列を持つ2つのページでしかなく、どちらが本人の公式サイトかを 判断する材料がありませんでした。順位ではなく、識別の問題です。

サイトに実装したもの

構造化データ (JSON-LD)
Person と WebSite を書き、sameAs で GitHub と X に紐付けた。書くのは名前・URL・アカウント・連絡先だけで、肩書きのような主観は入れない。
sitemap-index.xml / robots.txt
ビルド時に全ページのURLを列挙し、robots.txt からその場所を指している。
canonical / OGP
正規URLを明示し、SNSに貼られたときのカード表示も絶対URLで指定している。
JSなしで読める静的HTML
クローラーにはこれが最も確実な条件で、しかもSEOのために用意したものではない。演出を遅延ロードへ追い出した設計がそのまま効いている。
公開URLは mawo.dev のみ
自動割り当ての workers.dev URL は閉じ、同じ内容を別URLで配らない。マージ前に確認するためのPRプレビューだけは残している。

サイトの外で設定したもの

Search Console
DNSのTXTレコードで所有権を確認し、サイトマップを送信した。
GitHub リポジトリの改名
ドメインと同名だったリポジトリの名前を変えた。理由は下の意思決定ログにある。

ここだけは実測値がありません。順位は自分で決められる指標ではなく、反映までの時間も制御できません。 この節に書いたのは打った手であって、結果ではありません。 読めるようになった時点で数値を出します。外していたらそう書きます。

演出の Before / After

成果物一覧の看板が順に強く点灯し、微光へ落ち着くシーケンスを、その場で操作できます。stagger(看板間の遅延)とduration(1看板が点灯して落ち着くまでの合計尺)を変えると、 同じ演出がどれだけ印象を変えるかが分かります。

  1. たかがAI、されどAI。
  2. Omotenashi
  3. Owl Snap
  4. Smombie App
実際の値は stagger 0.09s / duration 0.48s(flash 0.14s + settle 0.34s)。看板は読める微光状態から、上から順に一度強く光って戻る。 値を大きくすると「点灯していく過程」が主役になりすぎ、看板そのものが待たされる。

意思決定ログ

採用した技術より、採用しなかった選択肢のほうが設計を語ります。 各エントリには必ず「検討したが選ばなかったもの」を含めています。

  1. なぜ Next.js ではなく Astro か

    判断したこと
    フレームワークに Astro を採用し、Next.js は採用しない。
    検討した選択肢
    • Next.js(App Router)サーバー機能(SSR/API Routes/ミドルウェア)が本サイトには不要。コンテンツは本質的に静的で、動的な処理はホバー演出や点灯シーケンスなどクライアント側の島だけで完結する。フルスタックフレームワークを選ぶ理由がない。
    • SvelteKit / Nuxt などの他フルスタックフレームワークNext.js と同じ理由に加え、Astro のアイランドアーキテクチャほど「JSをデフォルトゼロにする」思想が徹底されていない。
    決め手
    静的HTMLをビルド時に出力し、JSは必要な箇所だけ島として載せられること。演出過多なサイトほどベースの出力JSは軽くあるべきだと考えた。

    Next.js を使わない、と言うと「サーバーサイド処理をしないなら当然」と思われがちだが、実際に比較したのはビルド出力のデフォルト挙動だった。 Next.js は何もしなくてもクライアントランタイムが載る前提で設計されている。Astro は逆に、明示的に client: ディレクティブを書かない限りJSが出力されない。

    このサイトは§8のパフォーマンス予算(初期JS 30KB以下)を「マージ不可」の交渉不可条件として掲げている。 デフォルトの挙動がその予算に沿っているフレームワークを選ぶことで、予算を守るための追加の努力(バンドル分割、動的import の徹底など)を最小化できた。

  2. なぜ Lenis を入れなかったか

    判断したこと
    慣性スクロールライブラリ(Lenis)を導入しない。ネイティブスクロールのまま実装する。
    検討した選択肢
    • Lenis を導入し、全ページに慣性スクロールを適用するネイティブのスクロールイベントを乗っ取る実装のため、トラックパッド以外(マウスホイール、キーボード、スクリーンリーダーの読み上げ操作)での挙動が不自然になりやすい。INP(Interaction to Next Paint)にも悪影響が出る。
    • 特定セクションだけ Lenis を部分適用するスクロール挙動がページ内で切り替わること自体が違和感の原因になる。統一感を優先した。
    決め手
    「気持ちよさ」の対価としてコアウェブバイタルと操作感の一貫性を払う価値がないと判断した。成果物の看板が点灯する演出(GSAP ScrollTrigger)はネイティブスクロールの上でも十分に成立する。

    Lenis のようなスムーススクロールは、トラックパッドで触ると確かに気持ちがいい。マウスホイールのカクツキが消え、慣性が乗って「作られた質感」が出る。 ただしこれは体験の底上げではなく、入力デバイスによって体験差が生まれる実装でもある。

    このサイトの原則(§1.1)は「闇が主役で、光は例外である」というコントラストの設計であり、操作感を犠牲にしてまで得る演出的な気持ちよさとは方向性が違う。 削れる演出は削り、コアウェブバイタルを守るという判断はサイト全体で一貫させている。

  3. なぜ Three.js ではなく OGL か

    判断したこと
    背景シェーダー(光害の空)の実装に OGL を採用し、Three.js は採用しない。
    検討した選択肢
    • Three.jsフルスクリーン三角形1枚を描くだけの用途には機能範囲が広すぎる。シーングラフやローダーなど、使わない機能まで含む構成を選ぶ理由がない。
    • 生の WebGL API を直書きシェーダーのコンパイルやuniform管理などの定型処理まで自前で書くことになり、可読性と保守性が落ちる。得られる軽量化に対して割に合わない。
    決め手
    本サイトはWebGLをフルスクリーン三角形1枚のフラグメントシェーダーにしか使わない。必要な定型処理だけを扱える薄いラッパーで十分だった。

    WebGLの使用箇所は「光害の空」の1箇所のみで、しかもジオメトリはフルスクリーンを覆う三角形1枚だけ。カメラもライトもモデルローダーも要らない。

    この要件に対して Three.js のシーングラフは機能過多であり、§8の初期JS予算(30KB以下、ただしWebGLは遅延ロードで初期には含まない)に対しても、遅延ロード時点での転送量は小さいほうが望ましい。 OGL はWebGL周りの定型処理だけを薄くラップしており、この用途に対する過不足のなさで選んだ。

  4. なぜ Cloudflare Pages ではなく Workers か

    判断したこと
    ホスティングを Cloudflare Workers(Static Assets)にする。当初予定していた Pages は採用しない。
    検討した選択肢
    • Cloudflare Pagesこのサイトは完全な静的出力なので Pages でも過不足なく動く。設定ファイルすら要らない点はむしろ優れている。ただし Cron Triggers、Queue、Workers Logs、Email Workers、非ルートパスへの配信などが Pages では使えず、後から必要になった時点で移行作業が発生する。
    • Vercel / Netlify などの静的ホスティング配信品質に不満はないが、独自ドメインとDNSを既に Cloudflare で持っているため、事業者を分けると管理点が増える。分ける利点が見つからなかった。
    • GitHub Pages無料で単純だが、エッジ配信の品質と設定の自由度(ヘッダー、リダイレクト、将来の動的処理)で劣る。
    決め手
    静的配信の結果は Pages と Workers で変わらない。差が出るのは『後で動的処理が必要になったとき』だけで、そのとき移行コストを払うくらいなら、等価な今のうちに広いほうを選んでおく。支払う代償は wrangler.jsonc 一枚。

    当初の仕様では Pages を指定していた。理由は「静的配信で十分。エッジ配信が速い」で、これは事実として正しい。実際このサイトは SSR も API も持たないため、Pages で何も困らない。

    見直したきっかけは「なぜ Pages なのか」という問い自体だった。調べ直したところ、判断材料が当初と変わっていた。

    まず、Cloudflare は Pages を非推奨にしていない。ドキュメントに廃止の告知はなく、現役のプロダクトとして扱われている。一方で移行ガイドには「Unlike Pages, Workers has a distinctly broader set of features available to it」とあり、機能面では Workers がほぼ上位互換である。

    そして Pages の実質的な優位だった Git 連携とプルリクエストごとのプレビューデプロイは、Workers Builds が同等に提供するようになった。ここで差がなくなった。

    残る違いは wrangler.jsonc を1つ置くかどうかだけになる。この状態で「今は要らないから狭いほうでいい」と判断する理由が見つからなかった。

    なお、この判断は速度のためではない。静的ファイルを配る限り両者の配信結果は同じで、Lighthouse の数値も変わらない。選んでいるのは性能ではなく、将来の選択肢の広さである。

    このサイトは「機能を足さないこと」を繰り返し選んできたが、それは動くものを増やさないという意味であって、選べる余地を捨てるという意味ではない。前者はコストを減らすが、後者は将来のコストを増やす。

  5. なぜグローを限定して使うか

    判断したこと
    看板やネオン管など、光源として意味を持つ要素に限ってグローを使う。地の面や文字組み全体には広げない。
    検討した選択肢
    • 当初の原則禁止を維持するネオン街を主題にしながらサイト自身にはネオンがほぼなく、成果物を看板として読ませる表現とも噛み合わなかった。
    • 見出しやカードを含む主要要素をすべて光らせる光の希少性がなくなって定型表現へ戻り、背景が明るくなるぶん文字とのコントラストも落ちる。
    決め手
    禁止したかったのは発光ではなく、意味のない全面装飾だった。光る理由がある場所を選び、光をそこへ集中させれば『闇が主役で、光は例外』という原則を保てる。

    制作開始時の 2026 年 1 月 18 日には、グローを原則禁止し、ホバー中の窓セル1つだけを例外にした。制約は守れたが、ネオン街を題材にしたサイトからネオンそのものが消えていた。

    成果物一覧を「1作品 = 1枚の看板」に改めたことで、グローは飾りではなく情報を読ませる光になった。文字の芯を白に寄せ、色は内側・外側のグローと周囲への漏れ光に分ける。この規則は neon.css に集約している。

    解禁後も、光るのは成果物の看板、縦看板、選択中の導線などに限る。多色は使っても同じ強さで全面に広げず、文字が乗る場所では背景の最も明るい画素とのコントラストを測る。

  6. なぜ同名の GitHub リポジトリを改名したか

    判断したこと
    ドメインと同名だった GitHub リポジトリを portfolio-site に改名し、構造化データの sameAs でサイト・GitHub・X を同一人物の資産として明示する。
    検討した選択肢
    • 何もせず、自然に順位が入れ替わるのを待つリポジトリのほうが先に公開されており、github.com というドメイン自体の評価も比較にならない。放置して逆転する見込みが薄い。それ以上に、検索した人が最初に見るのがソースコードのリポジトリになる状態が、ポートフォリオとして本末転倒だった。
    • サイトのタイトルや見出しに mawo.dev を入れて、検索文字列との一致度を上げる検索エンジンに向けて命名を歪める行為で、サイトの名前が名前として意味を失う。順位のために中身を変え始めると、どこまでも変えられてしまう。
    • 被リンクを増やす(ディレクトリ登録、相互リンク)効果を自分で検証できない上、増やしたリンクは後から取り消せない。取り返しのつかない施策を、効果が読めないまま打つ理由がない。
    決め手
    衝突していたのは順位ではなく識別だった。同じ文字列を持つ2つの資産があり、どちらが本人の公式サイトかを示す材料が無い。ならば片方の名前を変えて衝突自体を消し、構造化データで『この3つは同じ人物のものだ』と機械に伝えるのが筋になる。サイトの中身は一文字も変えていない。

    公開直後、「mawo.dev」で検索するとサイトより先に GitHub のリポジトリが出てきた。リポジトリ名をドメインと同じ mawo.dev にしていたためである。

    最初はこれを順位の問題として捉えていた。どうすればサイトを上に持ち上げられるか、という捉え方である。これが間違っていた。

    検索エンジンの側に立つと、見えているのは同じ文字列を持つ2つのページでしかない。片方はソースコードの置き場で、もう片方は公開されたサイトである。両者が同じ人物のものだという情報がどこにもない以上、どちらを上に出すかは推測でしかない。順位を動かそうとする前に、識別の材料を渡していなかった

    打った手は2つだけである。リポジトリ名を portfolio-site に変えて文字列の衝突を消したこと。そして JSON-LD の PersonsameAs を持たせ、GitHub と X のアカウントを列挙したこと。後者は「このドメインの持ち主は、そのアカウントの持ち主と同じである」という宣言にあたる。

    書いたのは名前・URL・アカウント・連絡先だけで、肩書きや実績のような主観は入れていない。構造化データは文脈も根拠も添えられない形式なので、そこに検証できない情報を置くと、主張ではなく飾りになる。言いたいことがあるなら本文で書けばよい。

    技術的な下地はすでにあった。sitemap-index.xmlrobots.txt も canonical も最初から出している。何より、このサイトは JS を無効にしても全作品の本文とリンクが読める。クローラーにとってはこれが最も確実な条件で、しかも SEO のために用意したものではない。演出を遅延ロードへ追い出した設計が、そのまま効いているだけである。

    追記(2026年9月1日): 自動割り当ての *.workers.dev でも同じサイトが見えており、実際に別URLとして拾われたため、公開先を mawo.dev のみにした。canonical を書くだけでなく、重複する入口そのものを閉じた。PRごとのプレビューURLは、マージ前の確認に必要なので残している。

    最後に、この判断には実測値がないことを書いておく。フォントの分割も preload の絞り込みも、すべて測った結果として決めてきた。しかし検索順位は自分で決められる指標ではなく、反映までの時間も制御できない。ここに書いたのは打った手であって、結果ではない。外れていたらこの項目に追記する。