Article

SEO モバイルチートシート【一枚で要点把握】

高田晃太郎
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 requests

API = “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との二面管理により、ラボで再現できない地域差や端末差を補足できる。

パフォーマンス最適化の手順とベンチマーク

実装手順(推奨オーダー)

  1. HeroのLCP要素特定(DevToolsのLCP候補/Performanceパネル)と優先読み込み(priority/preload)。
  2. 画像最適化(WebP/AVIF、自動リサイズ、width/heightまたはaspect-ratio指定、lazy-loading)。
  3. CSS/JSの分割:Critical CSS抽出、非同期読み込み、unused削減、サードパーティの遅延実行。
  4. フォント最適化:subset、preload、display: swap、可変フォントの採用。
  5. 配信最適化:Brotli、HTTP/2/3、キャッシュ戦略(immutable/短期+revalidate)。
  6. 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-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 main

import ( “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ダッシュボードとバジェットの自動化まで到達できるだろう。あなたのプロダクトのテンプレートで、どの指標がボトルネックになっているか、今すぐ測って確かめよう。

参考文献

  1. Bryan McQuade, Barry Pollard. Defining Core Web Vitals thresholds. web.dev. https://web.dev/articles/defining-core-web-vitals-thresholds
  2. Google Search Central Blog. Rolling out mobile-first indexing. https://developers.google.com/search/blog/2018/03/rolling-out-mobile-first-indexing
  3. Google Search Central Blog. Evaluating page experience for a better web. https://developers.google.com/search/blog/2020/05/evaluating-page-experience
  4. web.dev. Optimize Cumulative Layout Shift (CLS). https://web.dev/articles/optimize-cls/
  5. MDN Web Docs. Viewport meta tag. https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag
  6. Google Search Central. Introduction to structured data. https://developers.google.com/search/docs/guides/intro-structured-data
  7. web.dev. INP is now a Core Web Vital (announcement, 2024). https://web.dev/blog/inp-is-a-core-web-vital/
  8. Chrome UX Report (CrUX) API documentation. https://developer.chrome.com/docs/crux/api
  9. Google Search Central Blog. Mobile-first indexing is complete (2023). https://developers.google.com/search/blog/2023/10/mobile-first-indexing-final
  10. 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
  11. StatCounter GlobalStats. Mobile vs Desktop market share worldwide. https://gs.statcounter.com/platform-market-share
  12. Lighthouse documentation. Performance budgets. https://github.com/GoogleChrome/lighthouse/blob/main/docs/performance-budgets.md
  13. web.dev. Optimize Interaction to Next Paint (INP). https://web.dev/articles/optimize-inp
  14. Google Search Central. Dynamic rendering. https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering
  15. Chrome Developers Blog. User-Agent Reduction. https://developer.chrome.com/blog/user-agent-reduction/