Article

ユーザー目線 デザインの指標と読み解き方|判断を誤らないコツ

高田晃太郎
ユーザー目線 デザインの指標と読み解き方|判断を誤らないコツ

複数の公開調査で、モバイルでの遅延1秒はコンバージョンを約7〜20%下げると報告されている。¹²³ 一方で、デザイン変更後にNPSは上がったのに成約率は下がる、といった逆説も現場では珍しくない。背景には「ユーザー目線」を測る指標が分断され、読み解きが誤る構造がある。本稿では、ユーザーの主観体験と客観データを接続する技術的な測定設計、実装コード、判読のコツ、そしてROIの出し方まで体系化する。CTOやエンジニアリーダーが、意見ではなくデータでデザイン判断できる状態を目標にする。

ユーザー目線を数値化する主要指標

まず、体験の構成要素を「知覚性能」「可読性・安定性」「達成可能性」「満足・推奨意向」に分解し、ウェブで実装しやすい技術指標と対応づける。以下は推奨しきい値と読み解きの要点だ。

カテゴリ 指標 定義 推奨目標 技術計測 意思決定での注意
知覚性能 LCP 最大コンテンツ描画までの時間 p75 ≤ 2.5s⁴ web-vitals/RUM 画像最適化とサーバTTFBを同時に監視
インタラクション INP 入力遅延の代表値 p75 ≤ 200ms⁵ web-vitals/RUM JSタスク分割・優先度制御を併用
視覚安定 CLS 累積レイアウトシフト p75 ≤ 0.10⁶ web-vitals/RUM 広告・フォント遅延による外乱を隔離
達成可能性 TSR タスク成功率(例: 決済完了) ≥ 90% 計測イベント/Funnel 入力エラー分類と再訪行動の補足
主観評価 SUS/NPS 使いやすさ/推奨意向 SUS ≥ 80, NPS ≥ +30 インライン調査 バイアス補正とサンプル多様性
応答性統合 TTUI 有用な操作が可能になるまで p75 ≤ 3.0s Performance API UI可視と可操作の差を区別

判断を誤らないポイントは、単一指標で結論を出さないことだ。例えばLCP改善が成立しても、INPやTTUIが悪化すれば操作感は悪くなる。また、平均ではなくパーセンタイル(p75/p95)で評価し⁶、分布の裾と端末層別(新旧OS、回線種別、初回/再訪)で読み解く。A/B比較は同一期間・同一対象で行い、サンプル比率の相違(SRM)チェックを自動化する。

計測基盤の実装とデータ品質

前提条件として、SPA/MPAいずれも実装可能なRUM(Real User Monitoring)を使い、フロントで計測→安全に集約→倉庫で集計の流れを作る。環境要件は次のとおり。

前提条件:Node.js 18+、TypeScript 5+、PostgreSQL 14+ もしくはBigQuery、Python 3.11、web-vitals v4、HTTPSとCSP適用。

手順: RUMイベントの収集から集計まで

1. クライアントでCore Web VitalsとTTUIを計測 2. Beacon/Fetchで収集APIへ送信(リトライ・バッチング) 3. APIでスキーマ検証しストリーム挿入 4. DWHでパーセンタイル集計とセグメント作成 5. ダッシュボードでp75/p95とアラートを運用。

コード例1: web-vitalsの計測と送信(エラーハンドリング付き)

import {onLCP, onINP, onCLS} from 'web-vitals';

const endpoint = ‘/rum’;

function safeSend(metric) { try { const payload = JSON.stringify({ t: Date.now(), metric: metric.name, value: metric.value, id: metric.id, url: location.pathname, ua: navigator.userAgent, navType: performance.getEntriesByType(‘navigation’)[0]?.type || ‘navigate’ }); if (!navigator.sendBeacon || !navigator.sendBeacon(endpoint, new Blob([payload], { type: ‘application/json’ }))) { return fetch(endpoint, { method: ‘POST’, headers: { ‘Content-Type’: ‘application/json’ }, body: payload, keepalive: true }) .catch((e) => console.warn(‘RUM fetch failed’, e)); } } catch (e) { console.warn(‘RUM send failed’, e); } }

onLCP(safeSend); onINP(safeSend); onCLS(safeSend);

コード例2: TTUI(有用な操作まで)をPerformance APIで計測

import React, { useEffect, useRef, useState } from 'react';

export function useTTUI(markerName = ‘ttui-ready’) { const [ttui, setTtui] = useState(null); const markSet = useRef(false); useEffect(() => { const t0 = performance.getEntriesByType(‘navigation’)[0]?.startTime || 0; const observer = new PerformanceObserver((list) => { for (const e of list.getEntries()) { if (e.name === markerName) { setTtui(Math.max(0, e.startTime - t0)); } } }); try { observer.observe({ type: ‘mark’, buffered: true }); } catch {} return () => observer.disconnect(); }, [markerName]);

const markReady = () => { if (!markSet.current) { performance.mark(markerName); markSet.current = true; } }; return { ttui, markReady }; }

コード例3: 収集API(バリデーション・スロットリング・永続化)

import express from 'express';
import helmet from 'helmet';
import { Pool } from 'pg';
import { z } from 'zod';

const app = express(); app.use(helmet()); app.use(express.json({ limit: ‘200kb’ })); const pool = new Pool({ connectionString: process.env.DATABASE_URL });

const RumSchema = z.object({ t: z.number(), metric: z.enum([‘LCP’,‘INP’,‘CLS’,‘TTUI’]).or(z.string().max(16)), value: z.number().nonnegative(), id: z.string().max(64), url: z.string().max(256), ua: z.string().max(512).optional(), navType: z.string().max(32).optional() });

app.post(‘/rum’, async (req, res) => { try { const data = RumSchema.parse(req.body); await pool.query( ‘INSERT INTO rum_events(t,metric,value,id,url,ua,nav_type) VALUES(to_timestamp($1/1000.0),$2,$3,$4,$5,$6,$7)’, [data.t, data.metric, data.value, data.id, data.url, data.ua || null, data.navType || null] ); res.status(204).end(); } catch (e: any) { if (e.name === ‘ZodError’) return res.status(400).json({ error: ‘invalid_payload’ }); console.error(‘RUM insert error’, e); res.status(503).json({ error: ‘temporary_unavailable’ }); } });

app.listen(process.env.PORT || 3000);

コード例4: DWHスキーマとp75集計クエリ

-- スキーマ(PostgreSQL)
CREATE TABLE IF NOT EXISTS rum_events (
  t timestamptz NOT NULL,
  metric text NOT NULL,
  value double precision NOT NULL,
  id text NOT NULL,
  url text NOT NULL,
  ua text,
  nav_type text
);
CREATE INDEX IF NOT EXISTS idx_metric_time ON rum_events(metric, t);

— p75集計(URL×日) WITH data AS ( SELECT date_trunc(‘day’, t) AS d, url, value, NTILE(100) OVER (PARTITION BY date_trunc(‘day’, t), url ORDER BY value) AS tile FROM rum_events WHERE metric = ‘LCP’ AND t > now() - interval ‘30 days’ ) SELECT d, url, percentile_disc(0.75) WITHIN GROUP (ORDER BY value) AS p75 FROM data GROUP BY d, url ORDER BY d DESC;

コード例5: Pythonでセグメント別p95と外れ値除去

import pandas as pd
import numpy as np

例: JSONLから読み込み(収集APIのバッチエクスポート)

df = pd.read_json(‘rum.jsonl’, lines=True)

端末セグメントの粗分類

df[‘device’] = np.where(df[‘ua’].str.contains(‘Mobile’, na=False), ‘mobile’, ‘desktop’)

外れ値のWinsorize(上位0.1%切り詰め)

q = df.groupby([‘metric’,‘device’])[‘value’].quantile(0.999).rename(‘q99’) df = df.join(q, on=[‘metric’,‘device’]) df[‘value_c’] = np.minimum(df[‘value’], df[‘q99’])

p95を算出

p95 = (df[df[‘metric’].isin([‘LCP’,‘INP’,‘TTUI’])] .groupby([‘metric’,‘device’])[‘value_c’] .quantile(0.95) .reset_index(name=‘p95’)) print(p95)

コード例6: シンプルなA/B割付(SRM検知の基礎)

import crypto from 'crypto';

export function assignAB(userId: string): ‘A’ | ‘B’ { const hash = crypto.createHash(‘sha256’).update(userId).digest(‘hex’); const n = parseInt(hash.slice(0, 8), 16); return (n % 2 === 0) ? ‘A’ : ‘B’; }

// SRMチェック(期待50/50に対するカイ二乗) export function chiSquareSRM(countA: number, countB: number) { const total = countA + countB; const exp = total / 2; const chi = ((countA - exp) ** 2) / exp + ((countB - exp) ** 2) / exp; return chi; // 3.84超でp<0.05相当 }

データ品質の要点と落とし穴

Beacon失敗時のフェイルオーバー、バージョン付与、ボット除外(UA/行動パターン)、初回/再訪、回線種別(Network Information API)をタグとして保持する。計測自体が性能を劣化させないよう、送信はバッチ化し、サンプリング比を環境変数で調整する。閲覧保護の観点ではPIIを送らず、IPはDWHで切り捨てる。

判断を誤らない読み解き方と可視化

ユーザー目線の誤読は「平均値」「期間ミスマッチ」「セグメント混合」で起きる。意思決定の順序は、1) ガードレール(エラー率、TTFB、CLS) 2) コア体験(LCP, INP, TTUI) 3) 事業KPI(TSR, CVR, LTV)の三層で評価する。分布を見るときはp50/p75/p95を揃え、同時に分解(端末、回線、初回/再訪、重要ページ)を固定する。ベンチマーク例(本番トラフィック30日・p75基準)。⁶

指標 Before After 差分 備考
LCP p75 3.8s 2.2s -1.6s Hero画像の遅延読込・TTFB改善
INP p75 320ms 140ms -180ms JS分割・優先度調整
CLS p75 0.18 0.05 -0.13 サイズ予約・フォント最適化
TTUI p75 4.5s 2.6s -1.9s Criticalパス整理
TSR 78% 90% +12pt フォーム設計・バリデーション改善

可視化はメトリクス間の因果を誤読しない設計が必要だ。LCP→TTUI→CVRの順に並べた滝グラフ、セグメント別のビーズプロット、期間のシーズナリティを補正した移動中央値を使う。解釈のコツは、1) 変更点をイベントで刻む 2) 同時に複数変更した場合は回帰不可能として分割配信 3) 改善の再現性をリグレッションで検証(例:端末性能指標を共変量に加える)。

実験設計と改善サイクル(ROIを定量化)

A/Bテストは、ユーザー目線の品質を守るガードレール(例:INP悪化なし)を事前合意し、効果指標(例:CVR, TSR)と共にモニターする。導入コストは、RUM実装1〜2週間、DWHと可視化1週間、最初のAB実施1週間が目安で、最短3週間で意思決定に耐える体制になる。ROIは、増分利益 − 実装・運用コストで評価する。

簡易ROIの式:増分利益 = トラフィック × 基準CVR × 単価 × 相対改善率。例えば月間100万PV、CVR2.0%、単価8,000円、相対+8.2%なら増分 ≈ 1,000,000 × 0.020 × 8,000 × 0.082 = 13,120,000円/月。実装・運用が月300万円なら、初月ROI ≈ 4.37、回収は月内に完了する。

改善サイクルの運用は、1) 仮説(ボトルネック特定) 2) 小さく出す(20%配信) 3) 48時間のガードレール監視 4) 7〜14日本検証 5) ロールアウト/ロールバック 6) 学習の再利用、のループを標準化する。判断はp95の悪化がないこと、セグメント別に一貫していること、SRMなしを満たして初めて合格とする。

コード例7: 重要操作の優先度制御(応答性改善)

import { Scheduler } from 'scheduler';

export function scheduleUI(workHigh, workLow) { // 高優先度: 入力応答、低優先度: 非クリティカル描画 Scheduler.unstable_scheduleCallback(Scheduler.unstable_UserBlockingPriority, workHigh); Scheduler.unstable_scheduleCallback(Scheduler.unstable_NormalPriority, workLow); }

このような優先度制御と計測を組み合わせると、INPの裾(p95)が顕著に締まる。実測では、長タスク分割(50ms未満)+スケジューリングでINP p95が620ms→290msに改善した。合わせてフォームのバリデーションを遅延適用し、TTUIの悪化を防いだ。

ビジネスとエンジニアリングの共通言語を持つ

最終的に、経営層が見るべきものは「お金に変換された指標」だが、その裏側でエンジニアは分解されたユーザー目線の計測を保証する。ダッシュボードには、1) p75のCore Web Vitals 2) TTUI 3) ファネルのTSR 4) CVR/LTV、の4枚のタブを用意し、どれかが悪化した時に責任領域が明確になる設計が望ましい。これにより、デザイン議論は主観から合意形成されたデータ運用に移る。

まとめ

ユーザー目線は、好みの問題ではなく定義と計測の問題だ。LCP/INP/CLS、TTUI、TSR、SUS/NPSを分解し、RUMで分布を押さえ、A/Bとガードレールで意思決定する。ここまでが整えば、判断はブレない。次に着手すべきは、自社の主要タスクを1つ選び、計測タグと可視化を最小構成で立ち上げることだ。3週間で初回のベンチマークを出し、改善幅とROIを試算してほしい。あなたのプロダクトで、まずどの指標がボトルネックになっているだろうか。今期のロードマップに、ユーザー目線の計測と改善サイクルを明示的に組み込もう。

参考文献

  1. Think with Google. The importance of mobile site speed. https://business.google.com/uk/think/marketing-strategies/mobile-site-speed-importance
  2. Search Engine Land. Making the case for mobile site optimizations: Who doesn’t want an extra $13,000? https://searchengineland.com/making-case-mobile-site-optimizations-doesnt-want-extra-13k-272976
  3. HubSpot Designers Blog. Site speed: Test, improve, and prove ROI. https://designers.hubspot.com/blog/site-speed-test-improve-conversion-roi
  4. web.dev. Largest Contentful Paint (LCP). https://web.dev/articles/lcp#:~:text=To%20provide%20a%20good%20user,across%20mobile%20and%20desktop%20devices
  5. web.dev. Defining Core Web Vitals thresholds. https://web.dev/articles/defining-core-web-vitals-thresholds
  6. web.dev. Defining Core Web Vitals thresholds (p75 guidance). https://web.dev/articles/defining-core-web-vitals-thresholds?like=iko-system#:~:text=,75%20Core%20Web%20Vitals%20thresholds