メインコンテンツへスキップ
コラム一覧に戻る
コラム

ECサイト改善の実践設計:TTFB・Core Web VitalsとCROを分けて検証する

2025年11月21日
最終更新: 2026年10月8日
12
ECサイト改善の実践設計:TTFB・Core Web VitalsとCROを分けて検証する

ECサイトが遅く、購入まで進む人も少ないとき、表示速度と購入導線を一緒に見直す価値があります。ただし、まとめて変更した前後の数字だけでは、何が購入に効いたかは分かりません。

この記事は、仮想のECサイトを使った改善設計と計算の教材です。特定企業の成果や「CVRが68%向上した実績」を示すものではありません。以下の訪問者、購入、売上、性能値、A/B結果は合成の例です。実績としての測定記録を提示できない数字を、成功保証として扱いません。

架空のEC画面と、表示速度を調べるトレー・購入導線を比べるトレーを分けた生成イメージ
AIで生成した概念画。実在サイトの画面や改善実績ではありません。

30秒で要点

  • 速度はTTFBと現行Core Web VitalsのLCP・INP・CLSで確認する。FIDは現在の指標へ換算できない。
  • 購入導線は同じ対象者のファネルで測り、商品情報、送料、入力エラーの仮説を確かめる。
  • A/Bテストは分母、割り付け、観測期間、主要指標、比較の数を先に決める。
  • 2.5%から4.2%は1.7ポイント増、相対68%増。同時に施策を変えた前後差だけでは、因果効果を示さない。
表示速度の観測と購入率の実験を分け、CVRのポイント差と相対差を区別するEC改善の手順
同じ改善計画でも、速度の観測と購入への効果検証を分けます。図は成果保証ではありません。

仮想のサイトと測定条件を定める

教材では、月間の対象者10万人、購入者2,500人、購入者1人につき1注文、平均注文額2万円を仮定します。対象者単位の購入率は2.5%、売上の計算値は5,000万円です。実店舗の報告値ではありません。

実際には、セッション・利用者・注文のどれを分母にするか、キャンセル・返品を含むか、観測期間を先に決めます。広告、端末、リピーター、商品構成、在庫、割引の変化も記録します。

表が収まらない場合は、横にスクロールできます

分けて測るもの記録する条件そこからは分からないこと
現場の表示性能端末、URL、期間、データ元、p75速度だけによる売上の増加
実験の購入率対象者、無作為割り付け、購入確認、観測期限対象外の利用者や季節の効果
エラー・返品・粗利同じ対象者と期間で集計CVRだけでは分からない副作用

速度の問題を測り直す

現行Core Web VitalsはLCP・INP・CLSです。良好の目安はLCP 2.5秒以下、INP 200ms以下、CLS 0.1以下で、端末別の75パーセンタイルを使います。INPは2024年3月12日にFIDを置き換えました。web.devの指標定義、INPへの移行

TTFBは最初の応答が届くまでの補助指標で、Core Web Vitalsそのものではありません。web.devは良好の目安を0.8秒以下としています。TTFBの定義

次のコードは測定APIではなく、値の単位を揃えた仮想の集計入力です。INP値は別に置いた仮想値で、昔のFIDから変換した値ではありません。

const baselinePerformance = {
  synthetic: true,
  aggregation: 'assumed_p75',
  segment: 'mobile-product-page',
  ttfbMs: 1200,
  lcpMs: 4500,
  inpMs: 320,
  cls: 0.25,
  fcpMs: 2800,
};

実際にはPageSpeed Insights等の現場データと、条件を揃えたラボの診断を分けて保存します。CrUX APIの集約窓は28日です。新しいページでは、期間表示が28日でも全期間にデータがあるとは限りません。更新直後の値は更新前の体験を含みえます。CrUX APIの集約条件

LCPをTTFB、画像などの読み込み開始までの遅れ、読み込み時間、描画待ちに分けると、調べる場所を絞れます。サーバーの応答だけを速くしても、画像の発見や描画が遅ければLCPは残ります。LCPの分解

表が収まらない場合は、横にスクロールできます

問題の仮説調べる証拠改善候補
サーバー・DBの処理が長いサーバー計測、クエリ計画、キャッシュの有無必要な列・件数、インデックス、公開情報のキャッシュ
主画像を見つけるのが遅いネットワークの開始時刻、HTMLHTMLから発見できる画像、適切な優先度
操作中に応答が止まる遅い操作の記録、長い処理処理・イベントの分割、不要な処理の削減
画面の位置が動くシフトの発生箇所画像の寸法、広告領域、フォントの計画

ファネルの数と仮説を分ける

次の合成例は、同じ期間の対象者を重複なく数え、各段階を順に通った人数として扱います。イベント発火の回数や、段階ごとに異なる母集団を混ぜる集計ではありません。

function summarizeFunnel(counts) {
  const keys = ['eligibleUsers', 'productUsers', 'cartUsers', 'checkoutUsers', 'buyers'];
  const values = keys.map(key => counts[key]);
  if (values.some(n => !Number.isSafeInteger(n) || n < 0) ||
      values.some((n, i) => i > 0 && n > values[i - 1])) {
    throw new Error('Use nonnegative, ordered counts from the same cohort');
  }
  const pct = (part, whole) => whole ? 100 * part / whole : null;
  return {
    purchasePercent: pct(counts.buyers, counts.eligibleUsers),
    productToCartDropPercent: counts.productUsers ? 100 - pct(counts.cartUsers, counts.productUsers) : null,
    cartToCheckoutDropPercent: counts.cartUsers ? 100 - pct(counts.checkoutUsers, counts.cartUsers) : null,
    checkoutToPurchaseDropPercent: counts.checkoutUsers ? 100 - pct(counts.buyers, counts.checkoutUsers) : null,
  };
}
const syntheticFunnel = {
  eligibleUsers: 100000, productUsers: 60000,
  cartUsers: 12000, checkoutUsers: 6000, buyers: 2500,
};
const funnelSummary = summarizeFunnel(syntheticFunnel);

この計算で購入率は2.5%、商品閲覧からカートまでの離脱は80%、カートから購入手続きは50%、購入手続きから購入は約58.33%です。離脱の理由は、この数だけでは分かりません。

送料、商品情報、在庫、入力エラー、決済の失敗を仮説として挙げ、観察や問い合わせ、エラー記録と照合します。ヒートマップで長く見ていた場所が、理解不足なのか関心なのかも単独では判断できません。入力内容や個人情報を録画・分析へ送らない設定と利用条件を確認します。

性能改善の実装を小さく切る

DBは値の連結と架空のキャッシュ設定を避ける

node-postgresはクエリ文字列と値を分けて渡します。次の関数は公開商品だけを最大20件取得する例です。dbは接続済みのクライアントまたはPoolを受け取ります。公式のparameterized query

async function listPublicProducts(db, categoryId) {
  if (!Number.isSafeInteger(categoryId) || categoryId <= 0) {
    throw new Error('Invalid category ID');
  }
  const result = await db.query(
    `SELECT id, name, price, image_url
       FROM products
      WHERE category_id = $1 AND is_public = TRUE
      ORDER BY created_at DESC, id DESC
      LIMIT $2`,
    [categoryId, 20]
  );
  return result.rows;
}

DBクライアントに存在しないuseCacheやcacheTTLを渡しても、キャッシュの設計にはなりません。必要なインデックスは、実際の件数とクエリ計画で確認します。比較は同じデータ量・条件で行い、キャッシュが温まった状態と未使用の状態を区別します。

公開商品ページをキャッシュしても、価格・在庫は鮮度を別に保ちます。ログイン後のカートや個人向け価格を共通キャッシュへ混ぜないでください。HTTP cacheの共有・認証条件

HTTP/2を使うこととServer Pushを勧めることも別です。ChromeはServer Pushの無効化を案内しています。不要な先読みを増やさず、実際の資源読み込みを測ります。ChromeのServer Push廃止案内

LCPの主画像と画面外の画像を分ける

以下の画像名は教材の例です。実際には、各幅に対応する画像を用意します。主画像をHTMLで発見できるようにし、寸法を指定します。LCP候補の主画像へloading="lazy"を付けません。

<picture>
  <source type="image/webp"
          srcset="product-480.webp 480w, product-960.webp 960w"
          sizes="(max-width: 600px) 100vw, 600px">
  <img src="product-960.jpg"
       alt="教材の商品:青い布製バッグの正面"
       width="960" height="640"
       fetchpriority="high" loading="eager">
</picture>

画面外の補足画像は別に遅延読み込みを検討します。Critical CSSの抽出やフォントの読み込みは、重複配信・表示崩れ・文字の置き換わりも測ります。フォントのswapだけでCLSが解決するとは限りません。広告やおすすめ領域も、必要な場所を先に確保してから内容を入れます。

CROでは判断に必要な情報を揃える

商品情報と送料を先に見せる

商品仕様、価格、配送条件、返品条件、確認できた在庫を示します。購入者数やレビューは根拠がある場合だけ表示し、架空の人数や切迫した文言で購入を促しません。

次は教材用の静的HTMLです。在庫や金額は表示例であり実在商品の値ではありません。実装ではサーバー側の正式な情報で差し替え、カート追加時にも価格と在庫を再確認します。

<section aria-labelledby="product-name">
  <h2 id="product-name">教材用の布製バッグ</h2>
  <p>表示例の価格:2,000円(税込)</p>
  <p>表示例の送料:500円。実際の配送条件は注文前に確認します。</p>
  <p>在庫表示例:3個。注文確定時に再確認します。</p>
  <form method="post" action="/example-cart-add">
    <input type="hidden" name="product_id" value="demo-bag">
    <label for="quantity">数量</label>
    <input id="quantity" name="quantity" type="number" min="1" max="3" value="1" required>
    <button type="submit">カートに追加する</button>
  </form>
</section>

/example-cart-addは読者が実装する教材用の送信先で、このHTMLに注文処理は含まれません。認証、CSRF、価格・在庫の再検証、二重送信や失敗時の扱いを別に実装します。色や文言を変更しても購入が増えるとは限らず、理解と操作のしやすさを確かめます。

カートと入力フォームを見直す

カートでは商品、数量、送料、合計、配送と返品の条件を購入前に確認できるようにします。証明のない認証バッジや「SSLだから安全」という説明を、支払い全体の保証にしません。

入力項目を必要なものに絞り、ラベル、入力形式、エラーの修正方法を揃えます。次はメール欄の静的例です。ラベルと入力欄を関連づけ、補足を読み取れるようにします。WAIのラベル

<form method="post" action="/example-checkout">
  <label for="checkout-email">連絡先メールアドレス(必須)</label>
  <input id="checkout-email" name="email" type="email"
         autocomplete="email" required aria-describedby="email-help">
  <p id="email-help">注文に関する連絡先を入力してください。</p>
  <button type="submit">入力内容を確認する</button>
</form>

ブラウザ側の入力検証は、サーバー側の検証を代替しません。送信後のエラーには場所と修正方法を示し、入力した内容を不必要に失わせないようにします。上の例は送信先・決済処理を含まない教材で、実機のキーボードや読み上げの確認も別に必要です。WAIの入力検証

決済方法やステップ表示は対象者の利用条件から選びます。ステップを減らすために必要な確認を省いたり、推測した残り時間を確定表示したりしないでください。

A/Bテストは結果を置く前に設計する

購入を主要指標にする場合、同じ人がA/Bをまたがない割り付け、対象者の重複除外、購入確認、観測期限を定めます。端末、広告、在庫や価格変更の条件も揃えます。途中の良い数字だけで止めず、事前に決めた方法で判断します。

例として、CTA、在庫表示、レビュー表示の3比較をひとつの検定族として事前に選んだと仮定します。以下は各群10,000人の合成結果です。レビューや在庫の情報が事実であることは、実験の前提です。

const syntheticExperiments = [
  { label: 'CTA', a: { users: 10000, buyers: 250 }, b: { users: 10000, buyers: 320 } },
  { label: 'stock', a: { users: 10000, buyers: 250 }, b: { users: 10000, buyers: 300 } },
  { label: 'review', a: { users: 10000, buyers: 250 }, b: { users: 10000, buyers: 290 } },
];

各10,000人という数を、どのテストでも十分な標本数とは考えません。想定する現状率、検出したい差、誤判定の許容、検出力から事前に検討します。

合成結果の計算を再現する

次はPython標準ライブラリだけで、独立した二群の購入率を正規近似で比較する例です。連続性補正なしの両側z検定と、Holmによる3比較の調整を計算します。小標本や希少事象ではこの方法をそのまま使わず、適切な検定・区間を選びます。比率z検定の条件、Holmの調整

import json
import math
from statistics import NormalDist

def compare_proportions(x_a, n_a, x_b, n_b):
    values = (x_a, n_a, x_b, n_b)
    if any(type(v) is not int for v in values) or n_a <= 0 or n_b <= 0:
        raise ValueError("integer counts and positive denominators are required")
    if not 0 <= x_a <= n_a or not 0 <= x_b <= n_b:
        raise ValueError("invalid buyer count")
    p_a, p_b = x_a / n_a, x_b / n_b
    pooled = (x_a + x_b) / (n_a + n_b)
    if min(n_a * pooled, n_a * (1 - pooled), n_b * pooled, n_b * (1 - pooled)) < 5:
        raise ValueError("normal approximation is unsuitable for this example")
    se_null = math.sqrt(pooled * (1 - pooled) * (1 / n_a + 1 / n_b))
    z = (p_b - p_a) / se_null
    p = math.erfc(abs(z) / math.sqrt(2))
    se_diff = math.sqrt(p_a * (1 - p_a) / n_a + p_b * (1 - p_b) / n_b)
    margin = NormalDist().inv_cdf(0.975) * se_diff
    return {"difference_pp": 100 * (p_b - p_a), "p_unadjusted": p,
            "ci95_unadjusted_pp": [100 * (p_b - p_a - margin), 100 * (p_b - p_a + margin)]}

def holm_adjust(pvalues):
    if any(type(p) not in {int, float} or not math.isfinite(p) or not 0 <= p <= 1 for p in pvalues):
        raise ValueError("invalid p-value")
    adjusted = [0.0] * len(pvalues)
    running = 0.0
    for rank, i in enumerate(sorted(range(len(pvalues)), key=lambda i: pvalues[i])):
        running = max(running, min(1.0, pvalues[i] * (len(pvalues) - rank)))
        adjusted[i] = running
    return adjusted

rows = [compare_proportions(250, 10000, b, 10000) for b in (320, 300, 290)]
adjusted = holm_adjust([row["p_unadjusted"] for row in rows])
for label, row, p_adj in zip(("CTA", "stock", "review"), rows, adjusted):
    print(json.dumps({"label": label, **row, "p_holm": p_adj}, ensure_ascii=False))

表が収まらない場合は、横にスクロールできます

合成の比較A→Bの購入率ポイント差未調整pHolm調整後p
CTA2.5%→3.2%+0.7約0.00293約0.00880
在庫表示2.5%→3.0%+0.5約0.03062約0.06125
レビュー表示2.5%→2.9%+0.4約0.08098約0.08098

この仮定と5%水準では、調整後に差を検出するのはCTAだけです。「3つとも有意」という結果にはなりません。コードの95%区間は各比較の未調整区間で、3比較を同時に覆う区間ではありません。p値だけで採用せず、差の大きさ、実装費用、返品・エラーなどの副作用を読みます。

CTAの相対28%、在庫の20%、レビューの16%を足して64%にしたり、それらを掛けて全体の成果としたりしません。相互作用や対象者の重複があり、同時の施策なら別の実験設計が必要です。

性能の変化と売上の計算を区別する

以下も仮想の性能入力です。比較の単位を保持し、相対減少率を計算します。実際の測定では、同じ端末・ページ・期間・集計条件を確認します。

const candidatePerformance = {
  synthetic: true, aggregation: 'assumed_p75', segment: 'mobile-product-page',
  ttfbMs: 500, lcpMs: 1800, inpMs: 180, cls: 0.08, fcpMs: 1500,
};
function relativeDecreasePercent(before, after) {
  if (!Number.isFinite(before) || !Number.isFinite(after) || before <= 0 || after < 0) {
    throw new Error('Use finite values in the same unit and a positive baseline');
  }
  return 100 * (before - after) / before;
}
const syntheticPerformanceChanges = Object.fromEntries(
  ['ttfbMs', 'lcpMs', 'inpMs', 'cls', 'fcpMs'].map(key => [
    key, relativeDecreasePercent(baselinePerformance[key], candidatePerformance[key])
  ])
);

合成例ではTTFB約58.33%、LCP60%、INP43.75%、CLS68%、FCP約46.43%の減少になります。これは算術であり、実測の報告や購入への因果効果ではありません。

CVRについても同じように、ポイント差と相対差を分けます。

function comparePurchaseScenario(users, beforeBuyers, afterBuyers, averageOrderYen) {
  if (![users, beforeBuyers, afterBuyers, averageOrderYen].every(Number.isSafeInteger) ||
      users <= 0 || beforeBuyers <= 0 || afterBuyers < 0 ||
      beforeBuyers > users || afterBuyers > users || averageOrderYen < 0) {
    throw new Error('Invalid synthetic counts');
  }
  const beforeRate = beforeBuyers / users;
  const afterRate = afterBuyers / users;
  return {
    differencePp: 100 * (afterRate - beforeRate),
    relativeIncreasePercent: 100 * (afterRate - beforeRate) / beforeRate,
    extraBuyers: afterBuyers - beforeBuyers,
    beforeRevenueYen: beforeBuyers * averageOrderYen,
    afterRevenueYen: afterBuyers * averageOrderYen,
  };
}
const syntheticPurchaseScenario = comparePurchaseScenario(100000, 2500, 4200, 20000);

対象者10万人と平均注文額2万円を固定した計算なので、購入者は1,700人増、売上は5,000万円から8,400万円になります。注文額や返品、粗利、獲得費用が変われば、この関係は変わります。仮想の2.5%→4.2%を「68%改善の実績」と呼ばないことが大切です。

段階的に変更して、続ける条件を決める

表が収まらない場合は、横にスクロールできます

段階実施すること続ける前に確認すること
現状と計測分母・期間・性能・購入確認を揃える重複や欠測、広告・在庫条件の変化
性能の修正一つずつ変更し、診断と現場値を保存描画崩れ、操作や購入のエラー
購入導線の実験事前設計したA/Bを行う割り付け、標本数、観測期限、比較の調整
判断と監視差と副作用、費用・粗利を確認返品・苦情・運用負荷、撤回する条件

表示速度とCROを同じ計画で扱っても、相乗効果は自動で生まれません。変更記録を残し、何を比較して判断したかを読めるようにします。季節や広告の変化を除けない前後比較は、その限界を報告します。

自社の最初の測定を決める

まず、同じ対象者の購入ファネルと、端末別の表示性能を保存してください。TTFBガイド、Webパフォーマンス完全ガイド、CRO完全ガイド、A/Bテストの標本設計と落とし穴、成果の出るWebサイト設計も判断の助けになります。

よくある質問(FAQ)

この記事の関連リンク

判断軸として

First byte Method 完全ガイド

AI×心理学×統計学で判断の質を高める実践的手法