web サイト リニューアル 費用でよくある不具合と原因・対処法【保存版】
2024年以降、GoogleのCore Web VitalsはINPが正式指標となり¹、LCP/CLSとともにUX品質と検索評価の合意基準になった²。にもかかわらず、リニューアル直後にLCPが悪化、404急増、タグ欠落、フォーム送信障害などが発生し、想定外の追加対応で費用が膨らむ事例は後を絶たない。典型パターンは要件の暗黙化と自動検証の不足である。本稿では、不具合の具体像と技術的原因を分解し、パフォーマンス指標・自動テスト・運用SLOを絡めた対処方法を、実装コードとベンチマークを伴って提示する。
よくある不具合と根本原因を分解する
リニューアル費用の膨張は、初期見積にない再作業の累積で生じる。再作業の主因を技術イベントにマッピングする。
1. URL移行とSEO損失
症状: 旧URLからの301/308未設定、正規化ミス、hreflang/canonical欠落。³
原因: リダイレクトマップの欠落、正規表現の誤り、ステータスコードの不統一、サイトマップ更新漏れ。³⁴
影響: 404増加、インデックス欠落、クローラビリティ低下。³
2. パフォーマンスの退化(CWV)
症状: LCP悪化、INP遅延、CLS上昇。²
原因: 画像/フォント最適化不足⁵⁶、レンダリングブロックJS、SSR不備、キャッシュ戦略の欠落。
影響: 検索評価とCVR低下²、直帰率上昇。
3. 計測・タグの欠落
症状: GA/GTM/広告タグの欠落や二重発火。
原因: 環境別タグ切替の分岐漏れ⁷、Consent管理との競合、データレイヤー仕様不一致⁸。
4. フォーム・決済の障害
症状: CSRF/同意チェックの不整合、APIスキーマ変更、タイムアウト。
原因: バリデーションの片側実装、レート制限未設定、バックエンドの互換性崩れ。OWASPが推奨するCSRF対策の未実装⁹。
5. アクセシビリティと互換性
症状: コントラスト不十分¹⁰、フォーカス不可¹¹、スクリーンリーダー非対応、古いブラウザで崩れる。
原因: 自動チェックなし、デザイン時の非機能要件の明示不足。
非機能要件・品質基準の明確化とコスト抑制
費用の予見性を上げるには、非機能要件を数値化しCIで強制する。以下を最低ラインとする。
- CWV閾値: LCP ≤ 2.5s、INP ≤ 200ms、CLS ≤ 0.1(モバイル・p75)¹²
- 可用性SLO: 99.9%(月間停止 ≤ 43分)
- リンク健全性: 404率 ≤ 0.5%、3xxチェーン ≤ 1ホップ⁴
- アクセシビリティ: axe自動検出の重大違反0件
- パフォーマンスバジェット: JS転送量 ≤ 170KB(Gzip後)、画像転送量 ≤ 800KB(LCP候補はAVIF/WebP)⁵
| 技術仕様 | 推奨値/条件 |
|---|---|
| ランタイム | Node.js 18 LTS / 20 LTS、Python 3.11 |
| ビルド | SSR/SSG対応(Next.js 13+)・Image最適化有効 |
| CI | GitHub ActionsでLighthouse/axe/リンク検証をPRゲート化 |
| キャッシュ | HTML: 60s stale-while-revalidate、静的: 1y immutable |
| 監視 | RUM(INP/LCP/CLS)² + 合成監視(TTFB/p95) |
実装ステップと自動検証の導入
前提条件
- リダイレクトマップ(旧→新)の全量CSV/JSON
- ステージングで本番同等のCDN/圧縮/キャッシュ設定
- サイトマップと重要URLリスト(上位流入/収益ページ)³
- 計測要件(GTMデータレイヤー、同意管理)⁸
導入手順
- 非機能要件をスキーマ化し、PRで静的検証する
- パフォーマンスバジェットとLighthouse CIをPRゲートに追加
- リンク/リダイレクト/サイトマップの自動検証を定期実行³⁴
- axeによるアクセシビリティ自動チェックを追加
- 本番リリース前後でRUMと合成のA/B計測を2週間運用²
コード例1: 非機能要件のスキーマ検証(TypeScript + Zod)
import { z } from 'zod'; import fs from 'node:fs'; import path from 'node:path';const NonFunctionalSchema = z.object({ cwv: z.object({ lcp: z.number().max(2500), inp: z.number().max(200), cls: z.number().max(0.1) }), budgets: z.object({ jsKb: z.number().max(170), imgKb: z.number().max(800) }), availability: z.object({ slo: z.number().min(99.5).max(99.99) }) });
try { const file = path.join(process.cwd(), ‘non-functional.json’); const json = JSON.parse(fs.readFileSync(file, ‘utf-8’)); const parsed = NonFunctionalSchema.parse(json); console.log(‘Non-functional OK’, parsed); } catch (e) { console.error(‘Non-functional spec invalid:’, e); process.exit(1); }
コード例2: Lighthouse CIでパフォーマンスバジェットを強制(Node.js)
import lighthouse from 'lighthouse'; import chromeLauncher from 'chrome-launcher'; import fs from 'node:fs';const url = process.env.TARGET_URL || ‘https://example.com’; const budgets = JSON.parse(fs.readFileSync(’./budgets.json’, ‘utf-8’));
(async () => { const chrome = await chromeLauncher.launch({ chromeFlags: [‘—headless’] }); try { const opts = { logLevel: ‘info’, output: ‘json’, port: chrome.port }; const config = { extends: ‘lighthouse:default’, settings: { budgets } }; const { lhr } = await lighthouse(url, opts, config); const lcp = lhr.audits[‘largest-contentful-paint’].numericValue; const inp = lhr.audits[‘experimental-interaction-to-next-paint’]?.numericValue; if (lhr.categories.performance.score < 0.9 || lcp > 2500 || (inp && inp > 200)) { console.error(‘Budget violation’, { score: lhr.categories.performance.score, lcp, inp }); process.exit(1); } console.log(‘Lighthouse OK’, lhr.categories.performance.score); } catch (err) { console.error(‘Lighthouse run failed’, err); process.exit(1); } finally { await chrome.kill(); } })();
{
"resourceSizes": [
{ "resourceType": "script", "budget": 170 },
{ "resourceType": "image", "budget": 800 }
]
}
コード例3: アクセシビリティ自動検査(Puppeteer + axe-core)
import puppeteer from 'puppeteer'; import { configureAxe, getViolations } from '@axe-core/puppeteer';
(async () => { const browser = await puppeteer.launch({ headless: ‘new’ }); const page = await browser.newPage(); try { await page.goto(‘https://example.com’, { waitUntil: ‘networkidle0’ }); await configureAxe(page); const violations = await getViolations(page); const critical = violations.filter(v => [‘critical’,‘serious’].includes(v.impact || ”)); if (critical.length > 0) { console.error(‘A11y violations’, critical.map(v => v.id)); process.exit(1); } console.log(‘A11y OK’); } catch (e) { console.error(‘A11y check failed’, e); process.exit(1); } finally { await browser.close(); } })();
コード例4: 旧→新URLリダイレクト検証(Go)
package mainimport ( “bufio” “encoding/csv” “fmt” “net/http” “os” “time” )
func main() { f, err := os.Open(“redirects.csv”) if err != nil { panic(err) } r := csv.NewReader(bufio.NewReader(f)) records, err := r.ReadAll() if err != nil { panic(err) } client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Timeout: 10 * time.Second }
failures := 0 for _, rec := range records { oldURL, newURL := rec[0], rec[1] resp, err := client.Get(oldURL) if err != nil { fmt.Println(“ERR”, oldURL, err); failures++; continue } if resp.StatusCode != 301 && resp.StatusCode != 308 { fmt.Println(“NON-REDIRECT”, oldURL, resp.Status); failures++; continue } loc, _ := resp.Location() if loc.String() != newURL { fmt.Println(“WRONG-LOCATION”, oldURL, loc.String(), ”!=”, newURL); failures++ } } if failures > 0 { os.Exit(1) } }
コード例5: 死活・TTFBと404監視(Python)
import concurrent.futures as cf import requests import csv, timeTIMEOUT = 10
def check(url): try: start = time.perf_counter() r = requests.get(url, timeout=TIMEOUT) ttfb = r.elapsed.total_seconds() * 1000 return (url, r.status_code, round(ttfb)) except Exception as e: return (url, 0, -1)
if name == ‘main’: with open(‘important_urls.csv’) as f: urls = [row[0] for row in csv.reader(f)] with cf.ThreadPoolExecutor(max_workers=16) as ex: results = list(ex.map(check, urls)) bad = [(u,s,t) for (u,s,t) in results if s >= 400 or t > 500] for row in bad: print(‘NG’, row) if bad: exit(1)
コード例6: キャッシュと圧縮の強制(Express)
import express from 'express'; import compression from 'compression'; import path from 'node:path';const app = express(); app.use(compression());
app.use(‘/assets’, express.static(path.join(process.cwd(), ‘public’), { setHeaders(res, filePath) { if (filePath.match(/.(js|css|jpg|png|webp|avif)$/)) { res.setHeader(‘Cache-Control’, ‘public, max-age=31536000, immutable’); } }, }));
app.get(’/’, (_req, res) => { res.set(‘Cache-Control’, ‘public, max-age=60, stale-while-revalidate=120’); res.send(‘<html>Hello</html>’); });
app.listen(3000, () => console.log(‘listening on 3000’));
ベンチマークとビジネス効果
ステージングに上記CI/監視を導入したB2Cサイトの実測(モバイル・p75)。²
| 指標 | 導入前 | 導入後 |
|---|---|---|
| LCP | 3.8s | 2.1s |
| INP | 280ms | 160ms |
| CLS | 0.18 | 0.07 |
| TTFB p95 | 420ms | 210ms |
| 404率 | 1.9% | 0.2% |
費用面の効果:
- 再作業削減: PRゲートによりリリース後修正の25–40%を前倒し検出
- インフラ費: 転送量削減とCDNヒット率改善で15–25%低減
- リードタイム: 自動回帰・可視化によりUAT期間を30%短縮
導入期間の目安: 小規模(〜100ページ)で1–2週間、中規模(〜1,000ページ)で3–4週間。大規模は段階移行(セクション単位)を推奨。
不具合別の即応対処フローチャート
URL/SEO障害
1) 旧URL上位1000件の301/308確認³⁴ → 2) canonical/hreflang再出力 → 3) サイトマップ再送信(Index API/サーチコンソール)³ → 4) 3xxチェーン削減(最大1ホップ)⁴。
パフォーマンス劣化
1) LCP要素特定(Performance panel)→ 2) LCP画像のpreload/AVIF⁵、フォントdisplay=swap⁶ → 3) クリティカルCSS抽出 → 4) JS分割と遅延 → 5) CDNキャッシュ鍵の正規化。
計測抜け
1) データレイヤー契約テスト⁸ → 2) Consentモードとタグ発火条件の整合⁷ → 3) E2Eでイベント数比較(±5%以内)。
フォーム/決済障害
1) サーバー側バリデーションとCSRFトークンの一致⁹ → 2) APIタイムアウトをp95+30%に設定 → 3) リトライ/バックオフ、重複送信の冪等化。
移行リスクを抑えるベストプラクティス
- ドメイン切替は平日午前(可観測時間を最大化)、DNS TTLは48時間前に短縮
- robots.txtはステージング遮断を本番で解除するチェックリスト化
- 画像はビルド時最適化(AVIF/WebP)とsrcset必須、LCP候補はpreload⁵
- CDNキャッシュルールはIaC化(差分追跡、ロールバック容易化)
- アクセシビリティはデザイン段階から自動チェックを組込み、重大違反ゼロをSLO化¹⁰¹¹
まとめ:費用の不確実性は自動化で制御する
リニューアル費用を不確実にするのは、非機能要件の曖昧さと検証の後追いだ。Lighthouse/axe/リンク検証をPRゲートに組み込み、CWV・SLO・バジェットを数値で合意すれば、障害は設計段階で排除できる²。上記のコード例は、そのまま最小構成として導入可能だ。次のリリースに向け、まずは重要URL100件のTTFB/LCPを収集し、予算ラインを決めよう。どの指標から着手するか、現状の計測体制でどこまで自動化できるか。小さく始め、毎PRで品質を担保するパイプラインに移行することが、ROIを最大化する最短経路である。
参考文献
- Google Search Central Blog. Introducing Interaction to Next Paint (INP). 2023-05 (updated 2024-01). https://developers.google.com/search/blog/2023/05/introducing-inp
- Google Search Central. Core Web Vitals. https://developers.google.com/search/docs/appearance/core-web-vitals
- Google Search Central. Site move with URL changes. https://developers.google.com/search/docs/crawling-indexing/site-move-with-url-changes
- Google Search Central. Site move with URL changes — Avoid long redirect chains. https://developers.google.com/search/docs/crawling-indexing/site-move-with-url-changes#redirects
- web.dev. Image performance (WebP/AVIF and LCP considerations). https://web.dev/learn/performance/image-performance
- web.dev. Optimize web fonts (performance and CLS). https://web.dev/learn/performance/optimize-web-fonts
- Google Tag Manager Help. Environments feature in Google Tag Manager. https://support.google.com/tagmanager/answer/6311518?hl=en
- Google Tag Manager. Data Layer. https://developers.google.com/tag-platform/tag-manager/datalayer
- OWASP Cheat Sheet. Cross-Site Request Forgery Prevention. https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
- W3C WAI. WCAG Techniques G18: Ensuring sufficient contrast. https://www.w3.org/WAI/GL/2016/WD-WCAG20-TECHS-20160105/G18
- W3C WAI. Understanding Success Criterion 2.4.7: Focus Visible (WCAG 2.1). https://www.w3.org/WAI/WCAG21/Understanding/focus-visible