ABOUT THIS SITE
派手な演出を持ちながら速度を落とさないこと自体が、このサイトの主張です。 実装は変わり続けるため、現在の構成と変えた理由を、数値と一緒に公開します。
実測値
最終計測: 2026/09/13 14:31 / Lighthouse (mobile, simulated throttling, gzip配信)
- 100Performance目標 95 以上
- 100Accessibility目標 100
- 100Best Practices目標 100(警告)
- 100SEO目標 100(警告)
| 指標 | 実測 | 目標 |
|---|---|---|
| LCP | 1.73s | 2.0s 以下 |
| CLS | 0.000 | 0.05 以下 |
| TBT | 0ms | 200ms 以下 |
| 初期JS (gzip) | 5.4KB | 30KB 以下(設計) |
| フォント合計 | 162.1KB | 200KB 以下 |
これらの数値は手打ちではありません。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 更新後はトップを再計測し、このページが読む実測値とフォントのサブセットを自動更新する。
こだわりと工夫
現在の実装へ至るまでに実際に踏んだ問題と、その対処です。一般論ではなく、このサイトで測って決めたことだけを載せています。
01
和文フォントを出力HTMLから逆算して切る
サブセット対象の文字を、ソースコードではなくビルド後のHTMLから集めている。ソースを走査すると日本語のコード内コメントまで拾い、描画されない文字のためにフォントが太る。Bold も本文全体ではなく、見出し・strong・表見出しに実際に出る文字だけから毎回作り直す。
02
和文をトップ用と残りに分割する
サイト全体の和文を1ファイルにすると、入口ページが「このサイトについて」の長文で使う文字まで読み込む。unicode-range で core / ext に分け、トップと成果物一覧は core だけで完結させた。このページの導入文だけは明示的に core へ加える。境界を広げると入口のLCPが悪化するため、実測で範囲を決めている。
03
欧文は1ウェイトに固定する
可変フォントは便利だが、LCP要素の書体では話が別だった。wght 400-700 の可変で20.1KB、ウェイト別2ファイルではリクエストが1本増える。どちらもLCPを1.86s→2.10sまで悪化させたので、wdth 80 / wght 700 の単一インスタンス(9.3KB)に固定した。
04
preload はページのLCP要素に絞る
書体を全部 preload すると帯域を奪い合って逆効果になる。共通では欧文見出し用の Archivo だけを先読みし、和文の導入文がLCPになるこのページだけ Zen Kaku Gothic New Regular を加えている。IBM Plex Mono は先読みしない。
05
アイランドの起動スクリプトを1本にまとめる
コンポーネントごとに script を置くと、小さなチャンクが別々のリクエストになる。成果物一覧では看板の点灯・光害の空・縦看板を1本の入口から起動し、GSAP と OGL は必要になった時点で動的 import する。さらに最初の操作か2.5秒経過まで観測自体を待ち、本文とフォントを先に配る。
06
GPUが無い環境では背景シェーダーを動かさない
GPUを持たない環境ではWebGLがソフトウェア描画になり、全画面のfBmシェーダーを毎フレームCPUで解くことになる。実際CIで Performance 0.71 / TBT 2180ms まで落ちた。内訳を見るとスクリプト実行は290msで、残りはラスタライズだった。描画元を検出して初期化を諦め、CSSのグラデーションに任せている。
07
計測は本番と同じ圧縮条件で行う
astro preview は無圧縮で配信するため、そのまま測ると本番より悪い数値が出る。Workers Static Assets の圧縮あり配信に合わせ、計測用に gzip を返す静的サーバーを用意した。ローカルとCIで同じ条件を使う。
08
窓グリッドをやめて看板にした
当初は3:4の窓を敷き詰め、65〜75%を装飾の消灯セルで埋めていた。作品名とサムネイルはホバー時のみ表示する設計だった。これが機能しておらず、静止状態では暗い四角が並ぶだけでどれが作品か分からない上、タッチ端末にはホバーが無いためタップするまで内容が不明だった。装飾セルは「空枠」に見えるだけで装飾として働いていなかった。比喩より機能を優先し、1作品=1枚の看板として名前と姿を常時見せる形に変えた。夜の街で実際に目に入るのは窓ではなく看板で、看板はそもそも読ませるために光っている。
09
ネオンの芯は白に寄せる
実物のネオン管は中心が白飛びしていて、色は周囲に出る。中心まで管の色のままだと「文字に影を付けただけ」に見える。芯・内側グロー・外側グロー・壁への漏れ光の4層で組んでいる。
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看板が点灯して落ち着くまでの合計尺)を変えると、 同じ演出がどれだけ印象を変えるかが分かります。
- たかがAI、されどAI。
- Omotenashi
- Owl Snap
- Smombie App
OSの「動きを減らす」設定が有効なため、再生を停止しています。
意思決定ログ
採用した技術より、採用しなかった選択肢のほうが設計を語ります。 各エントリには必ず「検討したが選ばなかったもの」を含めています。
なぜ Next.js ではなく Astro か
Next.js を使わない、と言うと「サーバーサイド処理をしないなら当然」と思われがちだが、実際に比較したのはビルド出力のデフォルト挙動だった。 Next.js は何もしなくてもクライアントランタイムが載る前提で設計されている。Astro は逆に、明示的に
client:ディレクティブを書かない限りJSが出力されない。このサイトは§8のパフォーマンス予算(初期JS 30KB以下)を「マージ不可」の交渉不可条件として掲げている。 デフォルトの挙動がその予算に沿っているフレームワークを選ぶことで、予算を守るための追加の努力(バンドル分割、動的import の徹底など)を最小化できた。
なぜ Lenis を入れなかったか
Lenis のようなスムーススクロールは、トラックパッドで触ると確かに気持ちがいい。マウスホイールのカクツキが消え、慣性が乗って「作られた質感」が出る。 ただしこれは体験の底上げではなく、入力デバイスによって体験差が生まれる実装でもある。
このサイトの原則(§1.1)は「闇が主役で、光は例外である」というコントラストの設計であり、操作感を犠牲にしてまで得る演出的な気持ちよさとは方向性が違う。 削れる演出は削り、コアウェブバイタルを守るという判断はサイト全体で一貫させている。
なぜ Three.js ではなく OGL か
WebGLの使用箇所は「光害の空」の1箇所のみで、しかもジオメトリはフルスクリーンを覆う三角形1枚だけ。カメラもライトもモデルローダーも要らない。
この要件に対して Three.js のシーングラフは機能過多であり、§8の初期JS予算(30KB以下、ただしWebGLは遅延ロードで初期には含まない)に対しても、遅延ロード時点での転送量は小さいほうが望ましい。 OGL はWebGL周りの定型処理だけを薄くラップしており、この用途に対する過不足のなさで選んだ。
なぜ Cloudflare Pages ではなく Workers か
当初の仕様では 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 の数値も変わらない。選んでいるのは性能ではなく、将来の選択肢の広さである。
このサイトは「機能を足さないこと」を繰り返し選んできたが、それは動くものを増やさないという意味であって、選べる余地を捨てるという意味ではない。前者はコストを減らすが、後者は将来のコストを増やす。
なぜグローを限定して使うか
制作開始時の 2026 年 1 月 18 日には、グローを原則禁止し、ホバー中の窓セル1つだけを例外にした。制約は守れたが、ネオン街を題材にしたサイトからネオンそのものが消えていた。
成果物一覧を「1作品 = 1枚の看板」に改めたことで、グローは飾りではなく情報を読ませる光になった。文字の芯を白に寄せ、色は内側・外側のグローと周囲への漏れ光に分ける。この規則は
neon.cssに集約している。解禁後も、光るのは成果物の看板、縦看板、選択中の導線などに限る。多色は使っても同じ強さで全面に広げず、文字が乗る場所では背景の最も明るい画素とのコントラストを測る。
なぜ同名の GitHub リポジトリを改名したか
公開直後、「mawo.dev」で検索するとサイトより先に GitHub のリポジトリが出てきた。リポジトリ名をドメインと同じ
mawo.devにしていたためである。最初はこれを順位の問題として捉えていた。どうすればサイトを上に持ち上げられるか、という捉え方である。これが間違っていた。
検索エンジンの側に立つと、見えているのは同じ文字列を持つ2つのページでしかない。片方はソースコードの置き場で、もう片方は公開されたサイトである。両者が同じ人物のものだという情報がどこにもない以上、どちらを上に出すかは推測でしかない。順位を動かそうとする前に、識別の材料を渡していなかった。
打った手は2つだけである。リポジトリ名を
portfolio-siteに変えて文字列の衝突を消したこと。そして JSON-LD のPersonにsameAsを持たせ、GitHub と X のアカウントを列挙したこと。後者は「このドメインの持ち主は、そのアカウントの持ち主と同じである」という宣言にあたる。書いたのは名前・URL・アカウント・連絡先だけで、肩書きや実績のような主観は入れていない。構造化データは文脈も根拠も添えられない形式なので、そこに検証できない情報を置くと、主張ではなく飾りになる。言いたいことがあるなら本文で書けばよい。
技術的な下地はすでにあった。
sitemap-index.xmlもrobots.txtも canonical も最初から出している。何より、このサイトは JS を無効にしても全作品の本文とリンクが読める。クローラーにとってはこれが最も確実な条件で、しかも SEO のために用意したものではない。演出を遅延ロードへ追い出した設計が、そのまま効いているだけである。追記(2026年9月1日): 自動割り当ての
*.workers.devでも同じサイトが見えており、実際に別URLとして拾われたため、公開先をmawo.devのみにした。canonical を書くだけでなく、重複する入口そのものを閉じた。PRごとのプレビューURLは、マージ前の確認に必要なので残している。最後に、この判断には実測値がないことを書いておく。フォントの分割も preload の絞り込みも、すべて測った結果として決めてきた。しかし検索順位は自分で決められる指標ではなく、反映までの時間も制御できない。ここに書いたのは打った手であって、結果ではない。外れていたらこの項目に追記する。