ページを開いたとき、理由はうまく説明できなくても「なんとなく散らかって見える」と感じることがあります。
文字色が少しずつ違う。
似た大きさの文字が何種類もある。
余白や角丸が場所ごとに違う。
こうした見た目のばらつきを、感覚ではなく0〜100の数字で確認できないかと考えました。
ところが、最初に作った採点方法を実在ページへ当てると、整って見える大手サイトほど悪い点になり、わざと壊したページほど良い点になりました。
計測処理は動いていました。
間違っていたのは、測り方そのものでした。
この記事では、値の種類数を数える方式がなぜ失敗したのか、集中度を使ってどう直したのか、さらに実サイトへ適用して踏んだ4つの誤検出について書きます。
最初の設計は「値の種類数」を数えるだけだった
最初は、ページ内で使われているCSSの値を集め、種類数がしきい値を超えたら加点する方式にしました。
対象にしたのは、文字色、背景色、文字サイズ、フォントファミリー、文字の太さ、余白、角丸、影、主要要素の左端です。
考え方は単純です。
たとえば文字サイズが数種類に収まっていれば整っていて、何十種類もあれば散らかっている、と判断します。
しかし、実際に測ると次の結果になりました。
| 対象 | 種類数で採点 |
|---|---|
| デジタル庁 | 52 |
| トヨタ自動車 公式 | 62 |
| NHK | 71 |
| Mozilla 日本語サイト | 80 |
| わざと壊した練習ページ | 29 |
順位が完全に逆です。
整って見える実在ページが52〜80点なのに、意図的に値をばらつかせた練習ページは29点でした。
ページが大きいほど点が上がっていた
原因は、種類数がページの規模に引っ張られることです。
ページが大きくなれば、見出し、注釈、カード、ナビゲーション、フッターなど、必要な役割も増えます。
役割が増えれば、使われる値の種類も自然に増えます。
同じ設計規則で作られていても、短いページより長いページのほうが不利になります。
つまり測っていたのは、見た目の散らかり具合ではなく、かなりの部分がページの大きさでした。
どのサイトにも同じ「下駄」が入っていた
もう一つ問題がありました。
余白と「主要要素の左端」は、実在ページなら必ずしきい値を超えていました。
ヘッダー、本文、カード、全幅セクション、フッターでは、必要な余白も左端の位置も違います。
そのため、どのサイトでもこの2項目が同じだけ加点されます。
サイト間の違いを表すはずの点が、全員へ一律に足される下駄になっていました。
しきい値を調整しても、本質的な問題は解決しません。
種類数そのものを採点する設計を捨てる必要がありました。
「種類数」ではなく「集中度」を測る
直した設計では、値が何種類あるかではなく、よく使われている少数の値がページ全体の何割を説明できるかを測ります。
手順は次のとおりです。
- 各値の出現回数を数える
- 出現回数の多い順に並べる
- 上位k個の出現回数を合計する
- 全出現回数に対するカバー率を出す
-
(1 - カバー率) × 100を項目点にする - 9項目の項目点を重み付き平均し、総合点にする
上位の値だけでほとんどを説明できれば、設計値が少数へ集約されているので点は低くなります。
逆に、上位から外れた値が長い裾を作っていれば点は高くなります。
ページの大きさに影響されにくい理由
この方式が見ているのは件数ではなく、covered / total という比率です。
同じ構成のまま要素数だけが2倍になれば、上位値の出現回数も全出現回数もおおむね2倍になるため、カバー率は変わりません。
長いページであること自体には加点せず、少数の値へどれだけ集中しているかだけを比較できます。
もちろん、ページ規模の影響が消えることと、デザインの良し悪しを完全に判定できることは別です。
実際の採点コード
実装の scoreScatter は次のようになっています。
export function scoreScatter(distributions, rawConfig = {}) {
const config = normalizeScatterConfig(rawConfig);
const metrics = {};
let weightedTotal = 0;
for (const key of Object.keys(SCATTER_METRICS)) {
const values = Array.isArray(distributions?.[key]) ? distributions[key] : [];
const counts = values.map(item => Math.max(0, Number(item?.count) || 0)).sort((a, b) => b - a);
const total = counts.reduce((sum, count) => sum + count, 0);
const covered = counts.slice(0, config.topK[key]).reduce((sum, count) => sum + count, 0);
const coverage = total > 0 ? covered / total : 1;
const score = rounded((1 - coverage) * 100);
const contribution = config.weights[key] * score / 100;
metrics[key] = { coverage: rounded(coverage * 100), score, contribution: rounded(contribution) };
weightedTotal += contribution;
}
const maximum = Object.values(config.weights).reduce((sum, value) => sum + value, 0);
const score = maximum > 0 ? Math.round(Math.min(100, weightedTotal / maximum * 100)) : 0;
return { score, metrics, contributions: Object.fromEntries(Object.entries(metrics).map(([key, metric]) => [key, metric.contribution])) };
}
空の項目はカバー率を1として、項目点を0にしています。
値が存在しないことを、散らかっているとは扱わないためです。
項目ごとの上位kと重みは次のとおりです。
| 項目 | topK | 重み |
|---|---|---|
| 文字色 | 4 | 14 |
| 背景色 | 4 | 17 |
| 文字サイズ | 5 | 14 |
| フォントファミリー | 2 | 8 |
| 文字の太さ | 3 | 9 |
| 余白 | 8 | 16 |
| 角丸 | 3 | 4 |
| 影 | 3 | 5 |
| 主要要素の左端 | 6 | 13 |
重みの合計は100です。
背景色と余白をやや重くし、角丸と影は低めにしています。
角丸や影の種類が少し増えることより、背景色や余白の規則が崩れるほうが、ページ全体の印象に影響しやすいと考えたためです。
文字に関わる項目は「文字数」で重み付けする
文字色、文字サイズ、フォントファミリー、文字の太さは、要素の個数をそのまま1票にはしていません。
その値で表示されている文字数を重みにしています。
見出し1行と本文1万字を同じ1票にすると、ページの大部分を占める本文の影響が過小になります。
反対に、短いラベルが多数並ぶ画面では、ラベル側の値が実際の見た目以上に強くなります。
実装では、要素が直接持つ文字列の長さを textWeight として4項目へ加えています。
if (text) {
const textWeight = [...text].length;
add("textColors", color(style.color), element, textWeight);
add("fontSizes", cleanNumber(style.fontSize), element, textWeight);
add("fontFamilies", family(style.fontFamily), element, textWeight);
add("fontWeights", String(style.fontWeight).toLowerCase(), element, textWeight);
}
これで、ページ上の文字を実際に多く担っている値ほど分布へ強く反映されます。
直したあとの実測
集中度方式へ変えて、同じ種類のページを測り直しました。
| 対象 | 種類数 | 集中度 |
|---|---|---|
| デジタル庁 | 52 | 2 |
| NHK | 71 | 3 |
| Mozilla 日本語サイト | 80 | 5 |
| トヨタ自動車 公式 | 62 | 7 |
| 自分がAIに作らせたLP | — | 26 |
| わざと壊した練習ページ | 29 | 55 |
種類数では29点だった練習ページが、集中度では55点になりました。
一方、種類数が80あったMozilla 日本語サイトは5点です。
値の種類が多くても、その大半が少数の主要値へ集中していれば、散らかり度は低く出ます。
自分がAIに作らせたLPは26点で、実在する大手サイト4件より明らかに高くなりました。
文字サイズの内訳を見る
総合点だけでは、何を直せばよいか分かりません。
そこで項目ごとに、上位値のカバー率、値の種類数、実際の値を残しています。
次は2026-09-07に実測した文字サイズの内訳です。
文字サイズ 点 37.9 (上位5値でページの 62.1% を説明。値の種類 22)
31.9% 16px
9% 11.5px
7.5% 14.4px
7.3% 14.1px
6.5% 13.4px
上位5値だけで説明できるのは62.1%で、残り37.9%が項目点になっています。
注目したいのは、14.4px と 14.1px が別々に存在していることです。
人が意図して設計した差なのか、生成や変換の途中で生まれた差なのかは、この数字だけでは決められません。
ただし、「ほぼ同じ値が別の設計値として残っている」と気づく入口にはなります。
実サイトへ当てて分かった4つの誤検出
散らかり度の採点を直しても、ページ検査全体が信頼できるとは限りません。
公開中のページへ当てると、もっと基本的な検査で大量の誤検出が出ました。
ここでは、実装を直すきっかけになった4例を、症状、原因、直し方に分けます。
1. 遅延読み込み画像を「表示されていません」と断定した
症状
自分のサイトを検査すると、18枚中12枚の画像が表示されていないという重い指摘が出ました。
ところが、ブラウザで手作業で数えると、その12枚はすべて正常に表示されていました。
原因
画面外にある loading="lazy" の画像は、読者がそこまでスクロールするまで読み込みが始まらないことがあります。
スクロール前に naturalWidth だけを見ると、まだ要求されていない画像と、本当に読み込みに失敗した画像を区別できません。
実装は「まだ読んでいない」を「壊れている」と断定していました。
直し方
上限時間を設けてページ末尾までスクロールし、先頭へ戻してから再度待つようにしました。
その後も complete === false なら、失敗とは断定せず「未読込で確認できない」と扱います。
complete === true && naturalWidth === 0 の場合だけ、画像が壊れていると判断します。
2. ティッカーの中身を「画面外にはみ出している」と数えた
症状
横方向へ流れるティッカーの文字が、画面外にはみ出した要素として大量に数えられました。
画面では、文字は入れ物の内側だけに見えており、意図した表示になっています。
原因
検査は要素自身の矩形と画面幅だけを比べていました。
しかしティッカーは、横長の中身を overflow-x: hidden の親要素で切り取り、その中身を動かします。
中身の矩形が画面より広いことだけを見れば異常に見えますが、入れ物による切り取りを含めれば正常です。
直し方
要素だけでなく、祖先に横方向を切り取る入れ物があるかを調べるようにしました。
アニメーションで動く要素は、画面外にある瞬間だけを根拠に指摘しません。
auto または scroll ならスクロール領域内、hidden または clip なら切り取り中として扱い、切り取る祖先がない場合だけ本当のはみ出しと判断します。
3. 全画面モーダルを、覆われた件数で見分けようとした
症状
年齢確認や同意の全画面モーダルが出ると、背後のボタンやリンクが「押せない」と指摘されました。
一度は、同じ前面要素が3件以上の操作要素を覆っていれば、モーダルとして1件にまとめる実装にしました。
しかし、背後の操作要素が2件しかないページでは、全画面モーダルなのに通常の被覆として残りました。
原因
モーダルかどうかを、覆われた側の件数で推測したことが原因です。
画面を覆う要素の性質は同じでも、ページ側のボタン数によって判定結果が変わっていました。
直し方
前面要素自身、またはその祖先の position と矩形を見るようにしました。
ビューポートの70%以上を覆う fixed、または前面にある absolute なら、背後の件数に関係なく全画面の遮蔽物とみなします。
個々のボタンごとに重複して出さず、初回表示を妨げる1件の指摘へまとめます。
4. 本文の条件数を、近くのリスト件数と突き合わせた
症状
本文にあった「7件の保存後は」という条件説明の数字を、近くにあった22項目のリスト件数と比較しました。
その結果、7と22が一致しないという重い指摘を出しました。
もちろん、その2つの数字には関係がありません。
原因
同じセクション内にある「N件」を、すべて近くのリスト件数の宣言だと決めつけていました。
数が何を指しているかという文脈を見ず、近さだけで対応付けたためです。
直し方
「以下のN件」「N件を紹介します」のように、数字とリストの対応が文中で明示されている場合だけ比較します。
条件、単価、期間を示す数字は対象から外します。
対応が曖昧な場合は不一致と断定せず、人による確認が必要な状態へ落とします。
4つを直した効果
公開中の自社サイト4本を最初の実装で検査したとき、「公開を止める」重さの指摘は105件出ました。
数字だけを見ると、すぐ直さなければならない状態です。
しかし、手で一つずつ確かめると、105件のうち104件が誤検出でした。
本物ではない指摘が大量に並ぶと、本当に見るべき1件が埋もれます。
上の4種類を直して、同じ4本を再び検査しました。
指摘は105件から1件になりました。
残った1件は本物でした。
「Google Play」ボタンが、存在しないページへ向いており、HTTP 404を返していました。
検出件数だけなら、修正前のほうが圧倒的に多く見つけています。
しかし、役に立ったのは修正後の1件です。
この数字の限界
散らかり度は、ページを見るための診断値であって、下げること自体が目的ではありません。
余白や色をきれいにそろえても、第一画面の約束が曖昧だったり、CTAが弱かったりすれば、ページは機能しません。
また、意図的に多色を使うブランドサイトは高く出ることがあります。
キャンペーンや作品の世界観を表現するために多くの色や文字組みを使っているなら、それは欠陥とは限りません。
数字だけで良し悪しは決まりません。
今回の実測では、整った大手サイト4件が2〜7、自分がAIに作らせたLPが26、わざと壊した練習ページが55でした。
この結果から、現時点では10以下を安定、25以上を要確認の目安にしています。
ただし、10以下/25以上という境界は、上の実測に基づく暫定値です。
対象の種類が増えれば、見直す必要があります。
まとめ
- 値の種類数を数えると、ページが大きいほど不利になり、順位が逆転する
- 上位k個のカバー率を使えば、ページ規模ではなく値の集中度を測れる
- 文字に関わる項目は文字数で重み付けし、実際の表示量を分布へ反映する
- 総合点だけでなく、カバー率と実値を残すと修正候補を追える
- 誤検出は、件数が多いほど本物の不具合を埋もれさせる
たくさん指摘が出る道具は簡単に作れます。
難しいのは、出た指摘が本物であることです。
105件を出した最初の実装より、誤検出を除いて本物のHTTP 404だけを残した1件のほうが、道具としてははるかに価値がありました。
設計した数字は、実在ページへ当て、内訳を見て、最後は人の目で確かめて初めて使えるものになります。
散らかり度の指標そのものは、AI検索での可視性を測る別の道具を作ったときの副産物です。そちらの計測方法は LLMO Scanner で公開しています。