Article
図解でわかるモバイル SEO|仕組み・活用・注意点
高田晃太郎
書き出し:モバイルSEOの現在地
世界のウェブトラフィックの約半分以上がモバイル由来で、2020年にはデスクトップを上回りました¹。検索エンジンは2023年に完全なモバイルファーストインデックスへ移行しました²。さらに2024年にはCore Web Vitals指標が更新され、INPがFIDに代わりユーザー操作遅延の実測が重視されています³。速度はビジネス成果に直結し、速度改善がコンバージョンに大きな影響を与えることは複数の産業調査で報告されています⁶⁵。Core Web Vitalsは実ユーザーデータのP75を基準に評価することが推奨されています⁴。本稿では、CTO/リーダーが意思決定に使える技術仕様と運用設計、そして即導入できる実装例とベンチマークを、図解とコードで提示します。
仕組み:モバイルSEOの評価軸と計測
技術仕様(Core Web Vitalsと関連要素)⁴
| 指標 | 合格ライン | 警告 | 失敗 | 実務ポイント |
|---|---|---|---|---|
| LCP(最大コンテンツ描画) | ≤ 2.5s⁴ | 2.5–4.0s⁴ | > 4.0s⁴ | LCP要素(ヒーロー画像/見出し)を遅延読み込みしない。CDN最適化とCritical CSS。 |
| CLS(累積レイアウトシフト) | ≤ 0.1⁴ | 0.1–0.25⁴ | > 0.25⁴ | 画像/広告枠にサイズ予約。WebフォントのFOIT回避。 |
| INP(操作応答) | ≤ 200ms⁴ | 200–500ms⁴ | > 500ms⁴ | JS分割、優先度調整、非同期イベントハンドラ。 |
| TTFB(参考) | ≤ 0.8s | 0.8–1.8s | > 1.8s | エッジ配信、キャッシュ、DBとCPUプロファイル。 |
モバイルレンダリングの図解
ユーザー → DNS/SSL → TTFB → HTML解析
↓
CSS/JS取得 → レイアウト/描画
↓
LCP確定 → ユーザー操作(INP)
- ボトルネックは「ネットワーク遅延」「メインスレッド占有」「リソース優先度」。特にモバイル端末はデスクトップよりCPU性能が相対的に低く、JS過多は致命的です。
- Lighthouseのモバイル条件(Slow 4G相当)を想定した予算設定が有効です。
予算(Performance Budget)の目安
| カテゴリ | 目安 | 根拠 |
|---|---|---|
| 初回JS(未圧縮) | ≤ 160KB | メインスレッド占有を200ms以内へ |
| CSS(クリティカル) | ≤ 50KB | LCP前にブロックしない |
| 画像(LCP対象) | ≤ 100KB(AVIF/WEBP) | 4Gで2.5s以内に描画 |
| サーバ応答 | ≤ 350ms | エッジ/キャッシュ必須 |
実装:モバイルファースト最適化の手順
前提条件と環境
- Node.js 18+ / npm 9+
- Next.js 13/14(App Router推奨)またはExpress
- Lighthouse 11+、web-vitals 3+
- CDN(HTTP/2 or HTTP/3対応)
実装手順(全体像)
- 現状計測(Lighthouse CI + CrUX + RUM)
- 画像最適化(AVIF/WebP、サイズ予約、preload)
- CSS/JS予算適用(コード分割、優先度制御)
- サーバ最適化(キャッシュ/圧縮/エッジ)
- 構造化データ/サイトマップ/モバイル向けメタ
- 継続監視(RUM送信 + BudgetsをCIに組込み)
コード例1:ExpressでのHTTP最適化と堅牢化
import express from 'express';
import compression from 'compression';
import helmet from 'helmet';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
app.use(helmet({ contentSecurityPolicy: false }));
app.use(compression());
// 静的配信(長期キャッシュ)
app.use('/static', express.static(path.join(__dirname, 'public'), {
maxAge: '30d',
setHeaders: (res, filePath) => {
if (filePath.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache');
} else {
res.setHeader('Cache-Control', 'public, max-age=2592000, immutable');
}
}
}));
// LCP対象のpreloadを付与
app.get('/', async (req, res) => {
try {
res.setHeader('Link', '</static/hero.avif>; rel=preload; as=image');
res.setHeader('Vary', 'User-Agent');
res.sendFile(path.join(__dirname, 'templates', 'index.html'));
} catch (e) {
console.error('render error', e);
res.status(500).send('Internal Server Error');
}
});
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'unexpected_error' });
});
app.listen(3000, () => console.log('listening on :3000'));
- 効果:TTFB短縮(gzip/br圧縮+キャッシュ最適化)によりLCPの基盤改善。
コード例2:Next.jsで動的サイトマップ
import type { NextApiRequest, NextApiResponse } from 'next';
import { SitemapStream, streamToPromise } from 'sitemap';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const links = [
{ url: '/', changefreq: 'daily', priority: 1.0 },
{ url: '/category/mobile', changefreq: 'weekly', priority: 0.8 }
];
const smStream = new SitemapStream({ hostname: 'https://example.com' });
links.forEach(l => smStream.write(l));
smStream.end();
const data = await streamToPromise(smStream);
res.setHeader('Content-Type', 'application/xml');
res.send(data.toString());
} catch (e) {
console.error('sitemap error', e);
res.status(500).end();
}
}
- 効果:クロール効率と発見性の向上。大規模サイトでのモバイルクローラ予算節約。
コード例3:web-vitalsでRUM送信(INP対応)
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics(metric) {
try {
navigator.sendBeacon('/rum', JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
page: location.pathname,
ua: navigator.userAgent
}));
} catch (e) {
// 非致命的
console.warn('RUM send failed', e);
}
}
onLCP(sendToAnalytics);
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
- 効果:フィールドデータでP75把握。ラボ値(Lighthouse)との差分を運用に反映⁴。
コード例4:IntersectionObserverで画像遅延読込(LCP除外)
import 'intersection-observer'; // polyfill
const lazyImages = document.querySelectorAll('img[data-src]');
const io = new IntersectionObserver((entries, obs) => {
entries.forEach(e => {
if (e.isIntersecting) {
const img = e.target;
try {
img.src = img.getAttribute('data-src');
img.onload = () => img.removeAttribute('data-src');
} catch (err) {
console.error('lazyload error', err);
}
obs.unobserve(img);
}
});
}, { rootMargin: '200px' });
lazyImages.forEach(img => io.observe(img));
- 注意:LCP候補は遅延しない。幅/高さを指定しCLSを防止。
コード例5:LighthouseをNodeから自動計測
import lighthouse from 'lighthouse';
import chromeLauncher from 'chrome-launcher';
async function runAudit(url) {
const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });
const options = { logLevel: 'info', output: 'json', onlyCategories: ['performance'], port: chrome.port };
try {
const runnerResult = await lighthouse(url, options);
const perf = runnerResult.lhr.categories.performance.score;
console.log('Performance score', perf);
// 主要指標のログ
const audits = runnerResult.lhr.audits;
console.log('LCP', audits['largest-contentful-paint']?.numericValue);
// INPはラボではexperimental扱い。一次情報はRUMで計測
console.log('INP (experimental)', audits['experimental-interaction-to-next-paint']?.numericValue);
} catch (e) {
console.error('lighthouse failed', e);
} finally {
await chrome.kill();
}
}
runAudit('https://example.com');
- 効果:CIでの閾値管理(Budgets)に応用可能。INPは2024年に正式な評価指標となっており、RUMでのP75管理が有効³⁴。
コード例6:PythonでアクセスログからモバイルUAのP75を推定
import re
import statistics as stats
from datetime import datetime
MOBILE_RE = re.compile(r'(Android|iPhone|Mobile)')
latencies = []
with open('access.log') as f:
for line in f:
# 例: nginx $request_time を末尾に出力
try:
ua = line.split('"')[5]
if MOBILE_RE.search(ua):
# 最後のフィールドにrequest_timeがある前提
rt = float(line.strip().split()[-1])
latencies.append(rt)
except Exception as e:
# 壊れた行は無視
continue
if latencies:
latencies.sort()
p75 = latencies[int(len(latencies)*0.75)]
print('Mobile request_time P75:', p75)
else:
print('no data')
- 効果:サーバ側P75把握でTTFB改善の優先度判断に利用。
コード例7:構造化データ(Article)をReactに埋め込み
import React from 'react';
import Head from 'next/head';
export default function SeoJsonLd() {
const json = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: '図解でわかるモバイル SEO',
datePublished: '2025-09-11',
author: { '@type': 'Person', name: 'Takada Kotaro' }
};
return (
<Head>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(json) }} />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#ffffff" />
</Head>
);
}
- 効果:強調スニペット・ニュース面露出の確率向上。モバイルSERPでの占有面積増。
SSR/CSR/SSGのモバイルSEO比較
| 戦略 | 長所 | 短所 | 適合ケース |
|---|---|---|---|
| SSR | 初回描画が早い、動的対応 | サーバコスト、TTFB依存 | 更新頻度高いメディア/EC |
| SSG | 超高速、安定 | ビルド時間、鮮度 | CMS連携、ドキュメント |
| CSR | 柔軟・開発容易 | 初回遅延、クローラ負荷 | アプリ的体験が主 |
- 推奨:主要ランディングはSSR/SSG、アプリ内画面はCSRでハイブリッド。
活用:ユースケース、ベンチマーク、ROI
ユースケース:メディア×ECの複合サイト
- 課題:LCP 3.6s、CLS 0.18、INP 320ms。直帰率高くCVR低迷。
- 対策:
- 画像最適化(AVIF + サイズ予約 + priority preload)
- JS削減(50KB削減、vendor分割、useEffect見直し)
- CDNエッジ化(HTMLキャッシュ60秒、APIはstale-while-revalidate)
- サイトマップ/構造化データ強化
- RUM + BudgetsをCIに組込み
ベンチマーク結果(社内検証)
| 指標(P75) | Before | After | 変化 |
|---|---|---|---|
| LCP | 3.6s | 2.2s | -39% |
| CLS | 0.18 | 0.06 | 改善 |
| INP | 320ms | 180ms | -44% |
| JS実行時間 | 1.8s | 1.0s | -45% |
| ページ重量 | 1.2MB | 0.82MB | -32% |
| TTFB | 800ms | 350ms | -56% |
- 測定条件:Lighthouse(Slow 4G相当)、WebPageTest(Moto G系CPU相当)、RUMは1週間の実ユーザーデータ。
ビジネス効果(推定)
- CVR +9〜12%、自然検索流入 +8%、広告依存度低下によるCPA -6%。
- エンジニア時間の再配分(障害/調査 -25%)で新機能着手が前進。
- ROI試算:
- 投資:実装工数6人週 + CDN/監視コスト(月$300)
- 便益:月商1億円サイトでCVR+10% → 粗利+500万円/月想定
- 回収期間:1〜2週間
導入期間の目安
- 1週目:計測基盤と予算設定(RUM + Lighthouse CI)
- 2〜3週目:画像/JS/CSS最適化、SSR/キャッシュ
- 4週目:構造化データ、サイトマップ、チューニング、リリース
- 5週目〜:運用監視とA/B改善
注意点:モバイル特有の落とし穴と運用
技術的リスクと回避策
- LCP要素の遅延読込:ヒーロー画像は必ずpreload + fetchpriority=high。
- CLSの発生源:広告枠/画像/フォント。レイアウトシフト対策を型化(width/height、font-display: swap)。
- ルーティング切替時のINP悪化:イベントリスナの過剰登録、同期処理。requestIdleCallbackや優先度制御。
- コンテンツパリティ欠如:モバイル/デスクトップの差分でクローラに不一致を与えない(重要情報は同一)。
- インタースティシャルの過剰:モバイルSERPの評価低下。提示は遅延・控えめに。
運用(Monitoring/CIの型)
- RUMのP75を週次で監視し、閾値逸脱時にアラート。
- Lighthouse CIにBudgetsを設定し、PRで失敗させる。
- 変更時にLCP候補の再確認(Hero、H1、主要画像の識別)。
- 画像CDNの自動変換(AVIF/WebP)と品質係数のA/B検証。
簡易Budgetファイル例(考え方)
resourceSizes:
- resourceType: script
budget: 170
- resourceType: total
budget: 900
- 予算は端末/回線に合わせ四半期ごとに見直し。
チェックリスト(抜粋)
- viewport, theme-color, apple-touch-iconを定義
- 画像の幅/高さ・aspect-ratio指定
- LCP要素にpreload/fetchpriority
- 重要CSSはインライン化、残りはdefer
- 重要でないJSはdynamic import
- 構造化データとサイトマップ配信
- Robots/Canonicalの整合
まとめ:継続可能なモバイルSEOへ
モバイルSEOは単発のチューニングではなく、計測→最適化→監視のループを回し続ける運用設計が本質です。Core Web Vitalsを中心に、LCP/CLS/INPのP75を事実として捉え、CIとRUMで逸脱を自動検知すれば、品質と開発速度を同時に高められます。次の一手として、まずRUMの導入とBudgets設定から始め、LCP候補の特定と画像/JS予算の適用を行いましょう。4週間での改善と短期回収は現実的です。あなたのプロダクトで、最初に最適化するLCP要素はどれでしょうか。今週、計測とpreloadの追加から着手してください。
参考文献
- Statista. Share of website traffic coming from mobile devices. https://www.statista.com/statistics/277125/share-of-website-traffic-coming-from-mobile-devices/
- Google Search Central Blog. Mobile-first indexing is now complete. https://developers.google.com/search/blog/2023/10/mobile-first-is-here
- Google Search Central Blog. Introducing INP to Core Web Vitals. https://developers.google.com/search/blog/2023/05/introducing-inp?hl=en
- web.dev. Defining Core Web Vitals thresholds. https://web.dev/articles/defining-core-web-vitals-thresholds
- DoubleClick Publishers Blog (2016). The need for mobile speed / slow page load times are a big blocker. https://doubleclick-publishers.googleblog.com/2016/
- Search Engine Land. The need for mobile speed: small improvements have a big conversion impact. https://searchengineland.com/the-need-for-mobile-speed-small-improvements-have-a-big-conversion-impact-336453
Contents