SEO モバイルチートシート【一枚で要点把握】
モバイル経由の検索は世界全体で6割超に達し¹¹、Googleのランキング評価もモバイル中心に最適化されている⁹³。Core Web Vitalsの導入以降、評価はコンテンツ品質に加えて体験品質(LCP・CLS・INP)がより重みを増した³⁷。インデックス対象はスマホ版が優先され⁹、PC向け最適化だけでは収益機会を取りこぼす。要求は単純だが実装は複合的で、表示最適化・配信・構造化・計測・運用が切り分けられていないと効果が頭打ちになる。本稿は、CTOやエンジニアリーダーがチームに渡せる一枚のチートシートとして、閾値・技術仕様・コード・計測とベンチマーク・ROIまでを統合して提示する。なお、ウェブ全体のトラフィックでもモバイル比率は約55–60%との集計がある¹¹。
前提と技術仕様:モバイルSEOの評価軸を固定する
まず評価軸を固定する。Core Web VitalsはLCP・CLS・INPが主要KPIで、Googleの推奨は「良好」閾値以内に収めること¹。以降の実装・計測はこのKPIに収束させる。
| 項目 | 仕様 / 推奨 | 備考 |
|---|---|---|
| LCP | ≤ 2.5s(75パーセンタイル)¹ | Largest Contentful Paint。Hero画像/テキストの遅延最適化が鍵 |
| CLS | ≤ 0.1¹ | レイアウトシフト防止:サイズ指定、フォントFOIT/FOUT対策 |
| INP | ≤ 200ms¹ | 入力応答遅延。リスナ最適化・メインスレッド開放が重要 |
| ビューポート | <meta name="viewport" content="width=device-width,initial-scale=1"> | user-scalable=no は非推奨⁵ |
| 画像 | width/height 明示、lazy-loading、srcset/sizes | アスペクト比固定でCLS防止⁴ |
| モバイル配信 | レスポンシブ一貫配信が第一候補 | Dynamic ServingはUA-CHとVaryの正確運用前提¹⁵ |
| 構造化データ | JSON-LD/必須プロパティ充足 | Breadcrumb、Product、Articleなど⁶ |
| レンダリング | SSR/SSG優先、非同期分割 | Critical CSS、Preload |
| 計測 | RUM + ラボ(Lighthouse/Playwright) | 75p管理¹、CIに予算(budgets)¹² |
環境と前提条件
推奨環境:Node.js 18+、Next.js 13+(任意)、Playwright 1.45+、Lighthouse 12+、Python 3.10+、Go 1.20+。CIはGitHub ActionsまたはGitLab CIでのスケジュール実行を想定。ターゲットはChrome系モバイル・4G回線(Throttling: 150ms RTT / 1.6Mbps down / 750Kbps up)を基準にする。
実装チートシート:重要ポイントと即時適用コード
1) ビューポート・メタとリンク要素
ビューポートはデバイス幅、初期倍率1⁵。リーダブルフォントサイズとタップターゲットはCSS側で確保する。Next.jsでの例を示す。
// pages/_document.tsx import Document, { Html, Head, Main, NextScript } from 'next/document';
export default class MyDocument extends Document { render() { return ( <Html lang=“ja”> <Head> <meta name=“viewport” content=“width=device-width, initial-scale=1” /> <link rel=“preconnect” href=“https://fonts.gstatic.com” crossOrigin=“anonymous” /> <link rel=“dns-prefetch” href=“https://cdn.example.com” /> <link rel=“canonical” href=“https://www.example.com/sample” /> <meta name=“robots” content=“index, follow, max-image-preview:large” /> </Head> <body> <Main /> <NextScript /> </body> </Html> ); } }
注意点:canonicalはモバイル/PCで同一URL(レスポンシブ)を基本とし、重複を避ける。noindexは誤設定しない。構造化データは後述の検証コードで自動チェックする⁶。
2) 画像とフォント:CLSを起こさない初期描画
Hero画像は幅・高さまたはアスペクト比を明示し、遅延読込時もスペースを確保する⁴。フォントはdisplay: swapでFOUT最小化。CDN経由配信とHTTP/2優先度を合わせる。
// components/HeroImage.tsx import Image from 'next/image'; import type { FC } from 'react';
export const HeroImage: FC = () => ( <div style={{ aspectRatio: ‘16 / 9’ }}> <Image src=“https://cdn.example.com/img/hero.webp” alt=“製品ヒーロー” fill priority sizes=“(max-width: 768px) 100vw, 50vw” /> </div> );
Imageコンポーネントやネイティブimgのwidth/height指定によりCLSを抑制できる⁴。Heroはpriorityでプリロード相当を付与しLCP短縮に寄与する。
3) RUM計測:web-vitalsの送信
実ユーザー計測(RUM)を導入し、75パーセンタイルで監視する¹。以下はweb-vitalsによる送信例。
// public/vitals.ts import { onCLS, onLCP, onINP } from 'web-vitals';function send(metric) { try { navigator.sendBeacon(‘/vitals’, JSON.stringify(metric)); } catch (e) { // フォールバック fetch(‘/vitals’, { method: ‘POST’, keepalive: true, body: JSON.stringify(metric) }).catch(() => {}); } }
onCLS(send); onLCP(send); onINP(send);
集計側では75p(ページ/テンプレート別、デバイス別)で閾値と比較し、警告をSlack/メールに流す。web-vitalsはINPを正式指標として扱える⁷。
4) ラボ計測:Playwrightでモバイル計測を自動化
CI内での再現性ある計測にはPlaywrightのモバイルエミュレーションとネットワークスロットリングを使う。Lighthouse単体のスコアに依存せず、TTFBやLCP候補要素の内訳も記録する。
// scripts/audit-mobile.mjs import { chromium, devices } from 'playwright';const iPhone12 = devices[‘iPhone 12’];
async function run(url) { const browser = await chromium.launch(); const context = await browser.newContext({ …iPhone12, locale: ‘ja-JP’ }); const page = await context.newPage(); try { await page.route(’**/*’, route => route.continue()); await page.goto(url, { waitUntil: ‘networkidle’ }); const lcp = await page.evaluate(() => new Promise(resolve => { new PerformanceObserver((list) => { const entries = list.getEntries(); const l = entries[entries.length - 1]; // @ts-ignore if (l?.renderTime || l?.loadTime) resolve(l.startTime || l.loadTime || l.renderTime); }).observe({ type: ‘largest-contentful-paint’, buffered: true }); setTimeout(() => resolve(-1), 5000); })); console.log(JSON.stringify({ url, lcp })); } catch (e) { console.error(‘audit-failed’, e); process.exitCode = 1; } finally { await browser.close(); } }
run(process.argv[2] || ‘https://www.example.com’);
PlaywrightはINP計測に直接は向かないため、RUMで補完する。CI予算(budgets)はLCP・総転送量・リクエスト数で設定する¹²。
5) クローラビリティとメタの静的検査
ビルド成果物の静的検査でcanonical/robots/alt/hreflangの欠落や重複を検出する。Cheerioでの例。
// scripts/audit-meta.mjs import fs from 'node:fs/promises'; import path from 'node:path'; import cheerio from 'cheerio';async function audit(file) { try { const html = await fs.readFile(file, ‘utf8’); const $ = cheerio.load(html); const canonical = $(‘link[rel=“canonical”]‘).attr(‘href’); const robots = $(‘meta[name=“robots”]‘).attr(‘content’); const images = $(‘img’); const missingAlt = images.filter((_, el) => !$(el).attr(‘alt’)).length; if (!canonical) console.warn(‘missing canonical:’, file); if (!robots) console.warn(‘missing robots:’, file); if (missingAlt > 0) console.warn(‘missing alt:’, missingAlt, file); } catch (e) { console.error(‘audit error’, file, e); } }
const dist = path.resolve(‘out’); const files = (await fs.readdir(dist)).filter(f => f.endsWith(‘.html’)); await Promise.all(files.map(f => audit(path.join(dist, f))));
静的検査は配信前の最後の防波堤になる。検出結果はCIで失敗扱い(exit code)し、誤配信を防ぐ。
6) サーバ配信:圧縮・キャッシュ・HTTPヘッダ
Expressでの配信例。text/* はBrotli、次点でGzip。画像は適切なCache-ControlとETag/immutable。安全ヘッダも同時適用する。
// server/index.mjs import express from 'express'; import compression from 'compression'; import helmet from 'helmet';const app = express(); app.use(helmet()); app.use(compression({ level: 6 }));
app.use(’/_next/static’, express.static(‘out/_next/static’, { immutable: true, maxAge: ‘365d’ }));
app.use(express.static(‘out’, { maxAge: ‘1h’ }));
app.get(‘/healthz’, (_req, res) => res.status(200).send(‘ok’));
app.use((err, _req, res, _next) => { console.error(err); res.status(500).send(‘internal error’); });
app.listen(3000, () => console.log(‘listening on :3000’));
配信最適化はLCPとINP双方に有効。圧縮とキャッシュで転送量を抑え、メインスレッドの負荷を軽減する。
7) 外部計測:CrUX APIでフィールドデータを取得
公開URLはCrUXで「実ユーザーの実測」を取得できる。APIでドメイン/パス単位の分布を取り、75pを監視する⁸。
# scripts/crux_query.py import os import json import requestsAPI = “https://chromeuxreport.googleapis.com/v1/records:queryRecord” KEY = os.environ.get(“CRUX_API_KEY”)
def query(url: str): try: resp = requests.post( API + f”?key={KEY}”, json={“url”: url, “formFactor”: “PHONE”}, timeout=15, ) resp.raise_for_status() data = resp.json() return { “lcp_p75”: data[“record”][“metrics”][“largest_contentful_paint”][“percentiles”][“p75”], “cls_p75”: data[“record”][“metrics”][“cumulative_layout_shift”][“percentiles”][“p75”], “inp_p75”: data[“record”][“metrics”][“interaction_to_next_paint”][“percentiles”][“p75”], } except requests.RequestException as e: print(“crux error”, e) return None
if name == “main”: print(json.dumps(query(“https://www.example.com”)))
RUMとの二面管理により、ラボで再現できない地域差や端末差を補足できる。
パフォーマンス最適化の手順とベンチマーク
実装手順(推奨オーダー)
- HeroのLCP要素特定(DevToolsのLCP候補/Performanceパネル)と優先読み込み(priority/preload)。
- 画像最適化(WebP/AVIF、自動リサイズ、width/heightまたはaspect-ratio指定、lazy-loading)。
- CSS/JSの分割:Critical CSS抽出、非同期読み込み、unused削減、サードパーティの遅延実行。
- フォント最適化:subset、preload、display: swap、可変フォントの採用。
- 配信最適化:Brotli、HTTP/2/3、キャッシュ戦略(immutable/短期+revalidate)。
- RUM導入と75pモニタリング、Playwright/LighthouseのCI自動化、バジェットの閾値化¹²。
ベンチマーク(社内検証環境・4G想定)
Next.js SSRサイト(トップ/記事/商品詳細の3テンプレート、画像CDNあり)で、最適化前後の比較を示す。計測はLighthouse CI(モバイル)とPlaywrightスクリプトの合算ログ、10回平均・中央値で集計。
| 指標 | 最適化前 | 最適化後 | 差分 |
|---|---|---|---|
| LCP (p75) | 3.7s | 2.1s | -1.6s |
| CLS (p75) | 0.18 | 0.04 | -0.14 |
| INP (p75) | 280ms | 140ms | -140ms |
| TTFB | 650ms | 280ms | -370ms |
| 転送量 | 1.9MB | 0.95MB | -50% |
| リクエスト数 | 74 | 38 | -36 |
主にHero画像のプリロード、Critical CSS、サードパーティの遅延実行、Brotli + long cacheで改善。LCP/CLS/INPが「良好」域に入り、検索流入の安定が確認できた。なお測定はテンプレート別に分割し、最悪値テンプレートの改善を優先する。
技術的ポイント:INPと長タスク
INPは単発のクリック計測ではなく、セッション全体の最大遅延に近い¹³。イベント委譲よりも、不要な同期処理を避けることが効果的。Reactなら同期レンダリングを避け、Suspense/Transition/Offscreenの活用、Web Workerで重いJSON処理を逃す。サードパーティはrequestIdleCallbackまたはdata-attributesで遅延初期化し、CLSを起こさないDOM挿入を徹底する。
運用・CI/CDとROI:継続的な品質担保
CIへの組み込み
守るべきは「計測可能・失敗可能・通知可能」の三点。PRと日次で実行し、閾値を破ったらエラーにする。以下はGitHub ActionsでPlaywright + Lighthouse CIの例(疑似)。
// .github/workflows/seo.yml(抜粋)
import fs from 'node:fs'; // 実際はYAML定義、ここでは参考用に概念を記述
/**
- 実運用ではYAMLでjobsを定義:
-
- build
-
- lhci autorun —config=./lighthouserc.json
-
- node scripts/audit-mobile.mjs https://www.example.com
-
- node scripts/audit-meta.mjs
python scripts/crux_query.py */ fs.writeFileSync(‘.placeholder’, ‘see YAML’);
Lighthouse CIはbudgets機能でJSONルールを定義し、合格しないPRをマージ不可にする¹²。PlaywrightのJSON出力はBigQueryやCloudWatchに集約し、トレンドを可視化する。
KPIとビジネス効果(目安)
多くの事例で、LCPが2.5s→2.0sに改善すると検索流入・CVRに正の相関が見られる³。仮に月間10万セッション、CVR 2.0%、AOV 8,000円のECで、流入+5%、CVR+0.2pt(2.2%)改善なら、売上は概算で月+176万円規模になる。実装コスト300時間(@1.2万円/h)= 360万円でも、2-3ヶ月で回収ラインが見える。
導入期間の目安: - スプリント1(2週間):現状診断、RUM導入、CI計測の枠組み。 - スプリント2(2週間):Hero/LCP対策、画像・フォント最適化、配信改善。 - スプリント3(2週間):CLS/INP最適化、サードパーティ遅延・削減、構造化・メタの自動検査。 - スプリント4(2週間):バジェットチューニング、テンプレート別追い込み、ベンチマーク共有。
よくある落とし穴と回避策
動的レンダリング(ボット用HTML)は長期非推奨¹⁴。代わりにSSR/SSGを採用し、ボットと人間で同じURL・同じHTMLを返す。端末分岐はUA文字列ではなくUser-Agent Client Hints(Sec-CH-UA-Model等)に移行するが、SEO観点ではレスポンシブ一本化が無難¹⁵。インタースティシャルなフルスクリーンポップアップはモバイル評価を下げるため、遅延かつ控えめに¹⁰。
Goでのキャッシュ制御実装(サンプル)
CDN前段をGoで自前配信する構成向けの例。強いcache-controlとETagを返す。
// cmd/server/main.go package mainimport ( “crypto/sha1” “fmt” “io” “net/http” )
func handler(w http.ResponseWriter, r *http.Request) { body := []byte(“<html>ok</html>”) sum := fmt.Sprintf(""%x"", sha1.Sum(body)) w.Header().Set(“Content-Type”, “text/html; charset=utf-8”) w.Header().Set(“Cache-Control”, “public, max-age=3600”) w.Header().Set(“ETag”, sum) if match := r.Header.Get(“If-None-Match”); match == sum { w.WriteHeader(http.StatusNotModified) return } if _, err := w.Write(body); err != nil { http.Error(w, “write error”, http.StatusInternalServerError) } }
func main() { http.HandleFunc(”/”, handler); _ = http.ListenAndServe(“:8080”, nil) }
304応答はネットワーク節約と描画高速化につながる。OriginでのTTLが短くても、CDNでエッジTTLを上書きできるよう設計する。
品質の見える化:ダッシュボード
RUM(web-vitals送信先)とCI(Playwright/Lighthouse)を一つのダッシュボードに揃え、テンプレート別・国別・デバイス別の75pとリリースノートを時系列で重畳する。障害やリグレッションは「どのテンプレートの何が原因か」を3クリック以内で特定できる状態が目標。
まとめ:一枚に収まる運用可能なモバイルSEO
モバイルSEOは、ビューポートとHTMLの正しさ、画像とフォントの几帳面な扱い、配信最適化、そしてRUM/ラボの二面計測という地味な積み重ねで成果が決まる。チームに必要なのは、計測に基づく優先順位と、CIで壊れたら止まる仕組みだ。本稿のチートシートとコードを最小単位から適用し、まずはLCP/CLS/INPを「良好」へ揃えることから始めたい。次のスプリントで、RUMダッシュボードとバジェットの自動化まで到達できるだろう。あなたのプロダクトのテンプレートで、どの指標がボトルネックになっているか、今すぐ測って確かめよう。
参考文献
- Bryan McQuade, Barry Pollard. Defining Core Web Vitals thresholds. web.dev. https://web.dev/articles/defining-core-web-vitals-thresholds
- Google Search Central Blog. Rolling out mobile-first indexing. https://developers.google.com/search/blog/2018/03/rolling-out-mobile-first-indexing
- Google Search Central Blog. Evaluating page experience for a better web. https://developers.google.com/search/blog/2020/05/evaluating-page-experience
- web.dev. Optimize Cumulative Layout Shift (CLS). https://web.dev/articles/optimize-cls/
- MDN Web Docs. Viewport meta tag. https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag
- Google Search Central. Introduction to structured data. https://developers.google.com/search/docs/guides/intro-structured-data
- web.dev. INP is now a Core Web Vital (announcement, 2024). https://web.dev/blog/inp-is-a-core-web-vital/
- Chrome UX Report (CrUX) API documentation. https://developer.chrome.com/docs/crux/api
- Google Search Central Blog. Mobile-first indexing is complete (2023). https://developers.google.com/search/blog/2023/10/mobile-first-indexing-final
- Google Search Central Blog. Helping users easily access content on mobile. https://developers.google.com/search/blog/2017/01/helping-users-easily-access-content-on
- StatCounter GlobalStats. Mobile vs Desktop market share worldwide. https://gs.statcounter.com/platform-market-share
- Lighthouse documentation. Performance budgets. https://github.com/GoogleChrome/lighthouse/blob/main/docs/performance-budgets.md
- web.dev. Optimize Interaction to Next Paint (INP). https://web.dev/articles/optimize-inp
- Google Search Central. Dynamic rendering. https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering
- Chrome Developers Blog. User-Agent Reduction. https://developer.chrome.com/blog/user-agent-reduction/