Article

web サイト リニューアル 費用でよくある不具合と原因・対処法【保存版】

高田晃太郎
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最適化有効
CIGitHub ActionsでLighthouse/axe/リンク検証をPRゲート化
キャッシュHTML: 60s stale-while-revalidate、静的: 1y immutable
監視RUM(INP/LCP/CLS)² + 合成監視(TTFB/p95)

実装ステップと自動検証の導入

前提条件

  • リダイレクトマップ(旧→新)の全量CSV/JSON⁳
  • ステージングで本番同等のCDN/圧縮/キャッシュ設定
  • サイトマップと重要URLリスト(上位流入/収益ページ)³
  • 計測要件(GTMデータレイヤー、同意管理)⁸

導入手順

  1. 非機能要件をスキーマ化し、PRで静的検証する
  2. パフォーマンスバジェットとLighthouse CIをPRゲートに追加
  3. リンク/リダイレクト/サイトマップの自動検証を定期実行³⁴
  4. axeによるアクセシビリティ自動チェックを追加
  5. 本番リリース前後で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 main

import ( “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, time

TIMEOUT = 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)。²

指標導入前導入後
LCP3.8s2.1s
INP280ms160ms
CLS0.180.07
TTFB p95420ms210ms
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を最大化する最短経路である。

参考文献

  1. 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
  2. Google Search Central. Core Web Vitals. https://developers.google.com/search/docs/appearance/core-web-vitals
  3. Google Search Central. Site move with URL changes. https://developers.google.com/search/docs/crawling-indexing/site-move-with-url-changes
  4. 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
  5. web.dev. Image performance (WebP/AVIF and LCP considerations). https://web.dev/learn/performance/image-performance
  6. web.dev. Optimize web fonts (performance and CLS). https://web.dev/learn/performance/optimize-web-fonts
  7. Google Tag Manager Help. Environments feature in Google Tag Manager. https://support.google.com/tagmanager/answer/6311518?hl=en
  8. Google Tag Manager. Data Layer. https://developers.google.com/tag-platform/tag-manager/datalayer
  9. OWASP Cheat Sheet. Cross-Site Request Forgery Prevention. https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
  10. W3C WAI. WCAG Techniques G18: Ensuring sufficient contrast. https://www.w3.org/WAI/GL/2016/WD-WCAG20-TECHS-20160105/G18
  11. W3C WAI. Understanding Success Criterion 2.4.7: Focus Visible (WCAG 2.1). https://www.w3.org/WAI/WCAG21/Understanding/focus-visible