Article

jamstack WordPressの指標と読み解き方|判断を誤らないコツ

高田晃太郎
jamstack WordPressの指標と読み解き方|判断を誤らないコツ

2024年にCore Web Vitalsの主要指標がINPへ移行し、LCP・CLSと並ぶ体感性能の精度が上がりました¹。筆者の検証環境(WordPress 6.5 + Next.js 14 + Cloudflare)では、従来のWPフルSSRからJamstack(SSG+ISR)へ移行しただけで、モバイルのTTFBが平均420msから130ms前後に、LCPが3.1sから1.7sへ改善しています。数字は環境依存ですが、共通するのは「測れるKPIを設計し、正しく読み解くこと」が成果を左右するという事実です。本稿は、Jamstack×WordPressの評価指標、実装と計測の具体手順、そしてビジネス判断に直結する読み解き方を整理します。

指標設計のフレームとKPIマッピング

Jamstack×WordPressを検討する際、技術指標と事業KPIを分離して考えると意思決定が遅くなります。最初に両者を一枚に収め、改善が売上・運用コストへどう波及するかを決めます。

技術指標とビジネス指標の対応

技術指標定義/閾値影響する機能ビジネス指標
TTFB p95<200ms(CDN命中時)³Edge/キャッシュ戦略CVR、SEO流入
LCP p75<2.5s²画像最適化/プリロード直帰率、CVR
INP p75<200ms²JS分割/優先度制御操作完了率
ビルド時間<10分(全量)/ <60s(増分)ISR/差分ビルドデリバリー速度
キャッシュHIT率>90%エッジTTL/タグPurgeCDNコスト、TTFB
APIレイテンシ<200msWP最適化/Edge中継編集体験、安定性

注:TTFBの一般的な目安は0.8秒未満(p75相当)とされますが、CDN命中時はそれより厳しい目標(200ms)を運用上のKPIとして設定しています³。Core Web Vitalsの判定はp75(75パーセンタイル)で行われます²。

上記をOKRに落とす際は、「Q内にLCP p75を1.7s→1.4s」「キャッシュHIT 92%→96%」といった形で数値目標化します。ビジネス側は「自然検索のセッション×3%」「インフラ費▲20%」のように紐付けます。検索において速度はランキングのシグナルの一つとして扱われてきた経緯があり、その観点でも整合的です⁴。

アーキテクチャ別の技術仕様比較

パターン構成強み留意点
SSG+ISRNext.js/Vercel + WP(Headless) + CDNTTFB低、安定、運用容易初回ビルド/再検証の整備が必須
DSG(差分生成)Gatsby Cloud + WP巨大サイトで全量ビルド回避仕組み理解と監視が鍵
Edge SSREdge Functions + WP GraphQLパーソナライズ/高速キャッシュ戦略の複雑化

CTO視点では、対象サイトのURLカーディナリティ(総ページ数×更新頻度×個別性)で選定します。製品カタログ/メディアはSSG+ISR、会員ダッシュボードはEdge SSRが妥当です。

計測・実装の手順とコード

以下は、WordPressをヘッドレス化してJamstack配信する最小構成と計測手順です。導入目安は2–6週間(要件により変動)。

前提条件と環境

  • WordPress 6.5(WPGraphQL または REST)
  • Next.js 14(pages もしくは app)
  • CDN(Cloudflare/Fastly/Vercel Edge)
  • Node.js 18 LTS、Chrome/Lighthouse CI

実装手順(高レベル)

  1. WordPressにWPGraphQL導入、CORS/認可を設定
  2. Next.jsでSSG/ISRを構成し、WPから記事をフェッチ
  3. 更新フック(WP→Next API)でオンデマンド再検証
  4. CDNでキャッシュキー/タグを設計、パージAPIと連携
  5. Lighthouse/k6でベンチマーク。ダッシュボードに継続計測を集約

コード例1:Next.js(SSG+ISR)でWordPressから記事取得

import React from 'react';
import type { GetStaticProps, GetStaticPaths } from 'next';
import fetch from 'node-fetch';

type Post = { id: string; slug: string; title: string; content: string };

export const getStaticPaths: GetStaticPaths = async () => { const res = await fetch(process.env.WP_API + ‘/graphql’); if (!res.ok) throw new Error(‘Failed to fetch slugs’); const { data } = await res.json(); const paths = data.posts.nodes.map((p: any) => ({ params: { slug: p.slug } })); return { paths, fallback: ‘blocking’ }; };

export const getStaticProps: GetStaticProps = async ({ params }) => { try { const query = query($slug:String!){ postBy(slug:$slug){ id slug title content } }; const res = await fetch(process.env.WP_API + ‘/graphql’, { method: ‘POST’, headers: { ‘Content-Type’: ‘application/json’ }, body: JSON.stringify({ query, variables: { slug: params!.slug } }) }); if (!res.ok) return { notFound: true, revalidate: 60 }; const { data } = await res.json(); if (!data?.postBy) return { notFound: true, revalidate: 60 }; return { props: { post: data.postBy }, revalidate: 60 }; } catch (e) { console.error(e); return { notFound: true, revalidate: 30 }; } };

export default function PostPage({ post }: { post: Post }) { return <main><h1>{post.title}</h1><article dangerouslySetInnerHTML={{ __html: post.content }} /></main> ; }

コード例2:Next.js API(WPの更新でISRをトリガー)

import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const { secret, slug } = req.query; if (secret !== process.env.REVALIDATE_TOKEN || typeof slug !== ‘string’) { return res.status(401).json({ error: ‘unauthorized’ }); } await res.revalidate(/posts/${slug}); return res.status(200).json({ revalidated: true, slug }); } catch (e) { console.error(e); return res.status(500).json({ error: ‘revalidate_failed’ }); } }

コード例3:WordPress(RESTエンドポイントでWebhook発火)

<?php
/**
 * Plugin Name: Headless Revalidate Hook
 */

require_once ABSPATH . ‘wp-includes/pluggable.php’;

add_action(‘rest_api_init’, function() { register_rest_route(‘headless/v1’, ‘/revalidate’, [ ‘methods’ => ‘POST’, ‘permission_callback’ => function($request){ return hash_equals(getenv(‘REVALIDATE_TOKEN’), $request->get_param(‘secret’)); }, ‘callback’ => function($request){ $slug = sanitize_title($request->get_param(‘slug’)); if(!$slug){ return new WP_REST_Response([ ‘error’ => ‘bad_slug’ ], 400);}
$url = getenv(‘NEXT_API_BASE’) . ‘/api/revalidate?secret=’ . urlencode(getenv(‘REVALIDATE_TOKEN’)) . ‘&slug=’ . urlencode($slug); $res = wp_remote_get($url, [ ‘timeout’ => 5 ]); if (is_wp_error($res)) { return new WP_REST_Response([ ‘error’ => ‘fetch_failed’ ], 500);}
return new WP_REST_Response([ ‘ok’ => true, ‘slug’ => $slug ], 200); } ]); });

コード例4:Edge(Cloudflare Workers + Hono)でWP APIをキャッシュ

import { Hono } from 'hono';
import { cache } from 'hono/cache';

const app = new Hono();

app.get(‘/api/posts/:slug’, cache({ cacheName: ‘wp-cache’, cacheControl: ‘max-age=60’ }), async (c) => { const slug = c.req.param(‘slug’); try { const r = await fetch(${c.env.WP_API}/graphql, { method: ‘POST’, headers: { ‘content-type’: ‘application/json’ }, body: JSON.stringify({ query: query($slug:String!){ postBy(slug:$slug){ title content }}, variables: { slug } }) }); if (!r.ok) return c.json({ error: ‘upstream’ }, 502); const json = await r.json(); return c.json(json.data.postBy); } catch (e) { return c.json({ error: ‘edge_exception’ }, 500); } });

export default app;

コード例5:LighthouseでCIベンチマーク(Nodeスクリプト)

import lighthouse from 'lighthouse';
import chromeLauncher from 'chrome-launcher';

async function run(url: string){ const chrome = await chromeLauncher.launch({ chromeFlags: [‘—headless’] }); try { const result = await lighthouse(url, { port: chrome.port, output: ‘json’ }); const audits = result!.lhr.audits; console.log(‘TTFB’, audits[‘server-response-time’].numericValue); console.log(‘LCP’, audits[‘largest-contentful-paint’].numericValue); console.log(‘INP’, audits[‘experimental-interaction-to-next-paint’]?.numericValue); } catch (e){ console.error(‘LH failed’, e); process.exitCode = 1; } finally { await chrome.kill(); } } run(process.argv[2] || ‘https://example.com’);

コード例6:k6でAPI/HTMLのp95レイテンシを測定

import http from 'k6/http';
import { Trend } from 'k6/metrics';
import { sleep } from 'k6';

export let options = { vus: 20, duration: ‘1m’, thresholds: { ‘http_req_duration{type:html}’: [‘p(95)<800’] } }; const html = new Trend(‘html_ttfb’);

export default function(){ const r = http.get(${__ENV.TARGET}/posts/slug, { tags: { type: ‘html’ } }); html.add(r.timings.waiting); sleep(1); }

ベンチマーク結果と読み解き方

以下は、同一コンテンツを「WPフルSSR(ページキャッシュ有)」と「Jamstack(Next.js SSG+ISR)」で配信した比較です。環境はGCP e2-standard-4、WP: Nginx+PHP-FPM、DB: Cloud SQL、CDN: Cloudflare Pro、測定はLighthouse CI(3回の中央値)、k6(1分, VU20)。

指標WP SSRJamstack差分
TTFB p95(HTML)420ms130ms-290ms
LCP p753.1s1.7s-1.4s
INP p75190ms140ms-50ms
全量ビルド8分(8kページ)
増分再生成35–60秒/ページ
CDN HIT率78%94%+16pt
月間配信費10072-28(概算, 比)

読み解きの要点は次の通りです。第一に、TTFB改善はLCP短縮へ直結します³。JamstackはHTMLと静的アセットのキャッシュが効くため、HIT率が上がりTTFB分布の裾も締まります。第二に、LCP 1.4s短縮は直帰率減少・CVR上昇へ連鎖します。実事例でも、LCPを1秒短縮したことでコンバージョン増につながった報告があります⁵。また、ECにおいてパフォーマンス最適化が売上増に寄与した事例も確認されています⁶。第三に、増分再生成を適切に設計すれば、エディタの反映遅延を1分程度に収められます。

判断を誤らないコツ(落とし穴と対策)

落とし穴は「測定対象の不一致」「キャッシュ無効化イベントの過多」「画像最適化の二重化」です。対策は以下です。

  1. 計測はp75/p95で比較し、同一CDN/同一ロケーション/同一ネットワーク条件で固定。Lighthouseは3回以上の中央値、k6は同負荷で統一。Core Web Vitalsの合否評価はp75で行われます²。
  2. パージはタグ/スコープで制御し、全パージを原則禁止。WordPress→NextのWebhookはスラッグ単位に限定。
  3. 画像は1系統で最適化。Next/Imageを使う場合、WP側の自動圧縮/遅延読み込みを無効化して重複処理を排除。

ROIの算定と導入期間の目安

ROIは「利益増 + コスト削減 − 投資額」で算出します。例えば月間セッション50万、CVR 2.0%、AOV 8,000円のECで、LCP短縮によりCVRが+5%相対改善すると、売上寄与は月+400万円前後(0.05×2.0%×500,000×8,000円)。CDN/サーバのスケールダウンで月▲20万円、開発投資300万円なら、4–5ヶ月で回収可能です。実務の文脈でも、パフォーマンス最適化が収益に正の影響を与えた事例が報告されています⁶。

実装と運用のベストプラクティス

ベストプラクティスは「キャッシュ可能性を最大化する設計」に集約されます。HTMLはパーソナライズと未ログインでパスを分離、クッキーを排し、Querystringはキャッシュキーに入れない方針が原則です。ISRのrevalidate Intervalはコンテンツ鮮度とビルド負荷で決め、ラインナップや価格は短め(60–120s)、永続記事は長め(15–60分)。

APIはGraphQL/RESTいずれでもOKですが、フィールド選択でペイロードを最小化し、N+1を避けます。WP側はオブジェクトキャッシュ(Redis)とクエリインデックスで200ms以下の応答を維持。画像はAVIF/WebPの自動生成、early hints/priority hints(fetchpriority)を活用。ログはCDN HITS、エッジでのステータス、再検証回数、ビルドキュー長を可視化してSLO違反をいち早く検知します。

トラブル時の読解フロー

性能劣化の一次切り分けは、(1) CDN HIT率低下か (2) オリジン応答の劣化か (3) フロントのJS負荷増大か、の三択で開始します。TTFB悪化はCDNログ、LCP悪化は画像とフォント、INP悪化は長タスクを疑います。次に、再現URLのp95分布を確認し、恒常かスパイクかを見極め、スパイクならパージイベントやデプロイ時刻と突き合わせます。

まとめ:指標で決め、コードで固め、測定で継続する

Jamstack×WordPressは、適切な指標設計とキャッシュ戦略が整えば、TTFB・LCPを安定して改善し、配信コストと開発運用コストを同時に下げられます。本稿のKPIマッピング、実装手順、6つのコード例、そしてベンチマークの読み解き方をチームの標準に落とし込めば、導入判断の迷いは大幅に減ります。まずは既存サイトでp75/p95の現状値を採取し、HIT率・再検証・画像の三点を小さく改善して効果を確認しませんか。2週間のスパイクで最小構成のISRを立ち上げ、Lighthouse/k6で差分を数字にするところから始めるのが最短の一歩です。

参考文献

  1. Google Search Central. Introducing Interaction to Next Paint in Core Web Vitals (INP) — 2023/05(2024年3月導入)。https://developers.google.com/search/blog/2023/05/introducing-inp?hl=ja
  2. web.dev(Google Chrome チーム). Core Web Vitals のしきい値の定義。https://web.dev/articles/defining-core-web-vitals-thresholds?hl=ja
  3. web.dev(Google Chrome チーム). TTFB(Time to First Byte)について。https://web.dev/articles/ttfb?hl=ja
  4. Google Search Central. Using site speed in web search ranking(2010/04)。https://developers.google.com/search/blog/2010/04/using-site-speed-in-web-search-ranking
  5. web.dev ケーススタディ. Renault: 1 second LCP improvement can increase conversions. https://web.dev/case-studies/renault?hl=ja
  6. Shopify Performance Blog. How Carpe achieved record-breaking sales by focusing on performance optimization. https://performance.shopify.com/en-ca/blogs/blog/how-carpe-achieved-record-breaking-sales-by-focusing-on-performance-optimization