load-testing-commerce について
開発者やECチーム向けに、ストアのトラフィック急増に備える実践的なスキルです。k6とArtilleryによる購買フローの再現、プラットフォーム別の確認、結果分析を扱います。
主な機能
- 01購買フローの現実的な再現
- 02k6とArtilleryの利用ガイド
- 03プラットフォーム別のテスト方法
- 04性能ベースラインと結果分析
- 05ステージングとテストデータの安全対策
活用シーン
- 01季節セールに向けたストア準備
- 02リリース前のチェックアウト性能確認
- 03ステージングでの負荷障害の再現
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ finsilabs/ awesome-ecommerce-skills --skill load-testing-commerceインストール後、Claude Code で/load-testing-commerceを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
tile.jsonevals/artillery-api-load-test/criteria.jsontask.mdevals/baseline-tracking-and-regression-detecti/criteria.jsontask.mdevals/k6-realistic-checkout-scenario/criteria.jsontask.md詳細
負荷テスト — コマース
概要
Eコマースアプリケーションの負荷テストでは、エンドポイントに同時リクエストを大量に送るだけでは不十分です。現実的なテストシナリオでは、カタログの閲覧、商品の検索、カートへの商品の追加、チェックアウトの完了など、実際のユーザー行動をシミュレートします。アクション間の思考時間も含めます。このスキルでは、k6 と Artillery で現実的なショッピングシナリオを構築する方法、結果を解釈してボトルネックを見つける方法、大規模セールの前にパフォーマンスのベースラインを確立する方法を扱います。
このスキルを使用する場面
- フラッシュセール、Black Friday、季節的なトラフィック急増に備える場合
- 大規模なインフラ変更(新しいデータベース、CDN、チェックアウトサービス)をデプロイする場合
- 新しいストアフロントのパフォーマンス SLO とベースラインを確立する場合
- 負荷が原因の本番環境のインシデントが発生し、ステージング環境で再現する必要がある場合
- 新しいチェックアウト機能をリリースするにあたり、パフォーマンスへの影響が不明な場合
基本的な手順
ステップ 1:プラットフォームとテスト可能な範囲を確認する
| プラットフォーム | 負荷テストの範囲 | 推奨される方法 |
|---|---|---|
| Shopify | Shopify のインフラは自動的にスケールするため、実質的に過負荷状態にすることはできません | Google PageSpeed Insights と Lighthouse CI でテーマのフロントエンドパフォーマンスをテストします。ホストするカスタムアプリやストアフロントは個別にテストします |
| WooCommerce | サーバーを自分で管理するため、セールイベント前の負荷テストが重要です | ステージングサイトに対して Loader.io(無料プラン:ターゲット 1 件、テストあたり 10K 接続)を使用するか、詳細なシナリオテストには k6/Artillery を使用します |
| BigCommerce | BigCommerce は自動的にスケールするため、プラットフォームのインフラは懸念事項ではありません | PageSpeed Insights でテーマのフロントエンドパフォーマンスをテストします。ホストするカスタムミドルウェアやヘッドレスレイヤーは個別にテストします |
| カスタム/ヘッドレス | 完全に制御できるため、負荷テストは自己責任です | 現実的なショッピングシナリオを用いて、ステージング環境に対して k6(オープンソース、スクリプトで設定可能)または Artillery(YAML ベース)を使用します |
ステップ 2:プラットフォーム別の負荷テスト
Shopify
Shopify のインフラは、事実上あらゆるトラフィック急増に対応できます。負荷テストでは、フロントエンドのユーザー体験に重点を置きます。
-
最も重要なページ(商品ページ、コレクションページ、チェックアウト)で Google PageSpeed Insights を実行します:
- pagespeed.web.dev にアクセスして、商品ページとコレクションページをテストします
- モバイルで Performance スコア 75 以上を目指します。セール前に赤色/オレンジ色の推奨事項に対処します
-
Shopify の組み込みパフォーマンスレポートを確認します:
- オンラインストア → テーマに移動し、レポートを表示をクリックします
- 実際のユーザーデータに基づく、ストアの Core Web Vitals(LCP、CLS、FID)が表示されます
- モバイルで LCP スコアが低い場合、ほとんどの場合、ヒーロー画像に
fetchpriority="high"を設定するか、圧縮する必要があります
-
セール前にインストール済みアプリを監査します:
- Shopify 管理画面のアプリに移動し、ストアフロントにスクリプトを挿入するアプリをすべて確認します
- サードパーティ製スクリプト 1 つにつき 50~200ms が追加されます。使っていないアプリを削除し、同期的に読み込まれるアプリは無効にします
-
Shopify Plus の場合 — 大規模なローンチ前に Shopify サポートへ連絡します:
- イベントの少なくとも 48 時間前に、Plus サポート窓口からフラッシュセールの通知を送信します
- Shopify はリソースを事前に割り当て、イベント中にストアを監視できます
WooCommerce
WooCommerce はお使いのホスティングインフラストラクチャ上で動作します。本番環境の構成(同じ PHP バージョン、同じ MySQL バージョン、同じキャッシュスタック)を再現したステージング環境でテストしてください。
Loader.io で手軽に負荷テストを実施する(コード不要):
- loader.io でサインアップします(無料プラン:ターゲット 1 件、テストあたり 10K 接続)
- New Test をクリックし、ステージング URL を入力します
- ランプテストを設定します。クライアント数 0 から開始し、60 秒かけて予想されるピークトラフィックまで増加させ、120 秒間維持します
- Loader.io から確認トークンが発行されます。所有権を確認するため、ステージングサイトのページに追加してください
- 特にチェックアウトページを対象にテストを実行します。商品閲覧は通常キャッシュされますが、チェックアウトではデータベースにアクセスするためです
結果の解釈:
- ピーク時の応答時間が 3 秒未満:許容範囲です
- エラー率が 1% を超える:PHP の致命的エラー、データベースのタイムアウト、メモリ不足がないか、サーバーログを調査してください
- 想定より低いトラフィックでもサーバーに負荷がかかる場合:セール前にホスティングのプランをアップグレードするか、Redis Object Cache を追加してください
テストの前に、ステージング環境のスタックが適切に構成されていることを確認してください:
- Redis Object Cache が有効になっている(Settings → Redis — ステータスが緑色)
- WP Rocket のページキャッシュが有効で、最も閲覧される商品ページのキャッシュが生成されている
- 本番環境で使用するものと同じサーバーサイズでテストしている
カスタム/ヘッドレス
カスタムストアフロントでは、本番環境を再現したステージング環境に対して k6 または Artillery を使用します。
開始前の重要事項:
- 必ずステージング環境でテストし、本番環境では決してテストしないでください
- テストモードの決済トークン(Stripe:
tok_visa)を使用してください。実際の決済処理サービスに対して負荷テストを実行しないでください - テスト注文には、後で一括削除できるよう、識別しやすいメールドメイン(例:
@test-load.invalid)を付けてください - 売り越しによってチェックアウトのシナリオが失敗しないよう、在庫数に上限のないテスト商品を用意してください
k6 による実際の買い物に近いシナリオ:
k6 は ramping-arrival-rate エグゼキューターを使って 1 秒あたりのリクエスト数を制御します(VU ベースの方法より実際の状況に近くなります)。一般的な E コマースのトラフィック分布:閲覧 60%、商品詳細 25%、カート 10%、チェックアウト 5%。
// k6/commerce-load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';
const BASE_URL = __ENV.BASE_URL || 'https://staging.mystore.com';
const products = new SharedArray('products', function() {
return JSON.parse(open('./data/products.json')); // array of {id, defaultVariantId}
});
export const options = {
scenarios: {
catalog_browsing: {
executor: 'ramping-arrival-rate',
startRate: 0, timeUnit: '1s',
preAllocatedVUs: 50, maxVUs: 200,
stages: [
{ target: 60, duration: '2m' }, // Ramp up
{ target: 60, duration: '5m' }, // Steady state
{ target: 0, duration: '1m' }, // Ramp down
],
exec: 'catalogBrowsing',
},
checkout_flow: {
executor: 'ramping-arrival-rate',
startRate: 0, timeUnit: '1s',
preAllocatedVUs: 10, maxVUs: 50,
stages: [
{ target: 5, duration: '2m' },
{ target: 5, duration: '5m' },
{ target: 0, duration: '1m' },
],
exec: 'checkoutFlow',
},
},
thresholds: {
'http_req_duration{scenario:checkout_flow}': ['p(95)<3000'],
'http_req_failed{scenario:checkout_flow}': ['rate<0.01'],
'http_req_duration{scenario:catalog_browsing}': ['p(95)<1000'],
},
};
export function catalogBrowsing() {
http.get(`${BASE_URL}/api/collections/featured`, { tags: { step: 'homepage' } });
sleep(2 + Math.random() * 3); // Think time: 2–5s
const categories = ['t-shirts', 'hoodies', 'accessories'];
const category = categories[Math.floor(Math.random() * categories.length)];
http.get(`${BASE_URL}/api/collections/${category}?page=1&sort=popular`, { tags: { step: 'category' } });
sleep(3 + Math.random() * 5);
}
export function checkoutFlow() {
const product = products[Math.floor(Math.random() * products.length)];
// View product
const productRes = http.get(`${BASE_URL}/api/products/${product.id}`, { tags: { step: 'view_product' } });
check(productRes, { 'product page 200': r => r.status === 200 });
sleep(2 + Math.random() * 3);
// Add to cart
const cartRes = http.post(`${BASE_URL}/api/cart`, JSON.stringify({
items: [{ productId: product.id, variantId: product.defaultVariantId, quantity: 1 }],
}), { headers: { 'Content-Type': 'application/json' }, tags: { step: 'add_to_cart' } });
check(cartRes, { 'add to cart 200': r => r.status === 200 });
const cart = JSON.parse(cartRes.body);
sleep(1 + Math.random() * 2);
// Start checkout
const checkoutRes = http.post(`${BASE_URL}/api/checkout/start`, JSON.stringify({
cartId: cart.id,
customer: { email: `test-${Math.random().toString(36).slice(7)}@test-load.invalid` },
}), { headers: { 'Content-Type': 'application/json' }, tags: { step: 'start_checkout' } });
check(checkoutRes, { 'checkout start 200': r => r.status === 200 });
const checkout = JSON.parse(checkoutRes.body);
sleep(5 + Math.random() * 10); // Think time: filling form
// Place order
const orderRes = http.post(`${BASE_URL}/api/checkout/complete`, JSON.stringify({
checkoutId: checkout.id,
paymentToken: 'tok_visa', // Stripe test token
shippingMethodId: checkout.shippingMethods[0]?.id,
}), { headers: { 'Content-Type': 'application/json' }, tags: { step: 'place_order' } });
check(orderRes, { 'order placed 201': r => r.status === 201 });
}
Artillery の YAML 設定(k6 の代替 — API を中心としたテストに適しています):
# artillery/commerce-load-test.yml
config:
target: "{{ $processEnvironment.BASE_URL }}"
phases:
- name: "Warm-up"
duration: 60
arrivalRate: 5
- name: "Ramp up"
duration: 120
arrivalRate: 5
rampTo: 50
- name: "Peak load"
duration: 300
arrivalRate: 50
- name: "Spike"
duration: 30
arrivalRate: 200
- name: "Recovery"
duration: 60
arrivalRate: 20
processor: "./processors/commerce-helpers.js"
scenarios:
- name: "Browse catalog"
weight: 60
flow:
- get:
url: "/api/collections/all?page=1"
capture:
json: "$.products[0].id"
as: "productId"
- think: 3
- get:
url: "/api/products/{{ productId }}"
- name: "Complete checkout"
weight: 10
flow:
- function: "generateCheckoutData"
- post:
url: "/api/cart"
json:
productId: "{{ productId }}"
quantity: 1
capture:
json: "$.id"
as: "cartId"
- think: 8
- post:
url: "/api/checkout/start"
json:
cartId: "{{ cartId }}"
email: "{{ email }}"
capture:
json: "$.checkoutId"
as: "checkoutId"
- think: 10
- post:
url: "/api/checkout/complete"
json:
checkoutId: "{{ checkoutId }}"
paymentToken: "tok_visa"
expect:
- statusCode: 201
テストを実行してベースラインを記録する:
k6 run \
--env BASE_URL=https://staging.mystore.com \
--out json=results/baseline-$(date +%Y%m%d).json \
k6/commerce-load-test.js
タグを使って各ステップの結果を分析する:
# Overall summary (k6 outputs this automatically at end of run)
# Per-step breakdown from the JSON output
cat results/baseline-*.json | jq '
[.data_points[] | select(.type=="Point" and .metric=="http_req_duration")]
| group_by(.tags.step)
| map({step: .[0].tags.step, p95_ms: (map(.value) | sort | .[length * 0.95 | floor])})
'
E コマース向けのスケーリング目標:
- カタログページの p95 < 1000ms
- 商品詳細の p95 < 1500ms
- チェックアウトフローの p95 < 3000ms
- ピーク時のエラー率 < 1%
主要リリースの前に CI で負荷テストを実行する(GitHub Actions):
# .github/workflows/load-test.yml
name: Load Test (Pre-Release)
on:
workflow_dispatch:
inputs:
target_url:
description: 'Staging URL to test'
required: true
default: 'https://staging.mystore.com'
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install k6
run: |
curl -s https://dl.k6.io/key.gpg | sudo apt-key add -
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install k6
- name: Run load test
env:
BASE_URL: ${{ github.event.inputs.target_url }}
run: k6 run --env BASE_URL=$BASE_URL --out json=results.json k6/commerce-load-test.js
- uses: actions/upload-artifact@v4
with:
name: load-test-results
path: results.json
ベストプラクティス
- ステップの間に思考時間を設ける — 実際のユーザーは操作の合間に間を置きます。思考時間をなくすと、現実離れしたリクエスト率になり、本番環境のパターンを反映できません
- テストモードの決済トークンを使う — 必ず Stripe の
tok_visaまたは同等のテストトークンを使用してください。実際の決済処理サービスに対して負荷テストを実行しないでください - 本番環境ではなくステージング環境で実行する — 負荷テストはリソースを消費し、実際の顧客向けサービスの品質を低下させる可能性があります
- 予想されるピークの 1.5 倍、2 倍、3 倍の負荷で測定する — 実際のセール前にシステムの限界点を見つけるため、異なる負荷レベルで複数のテストを実行してください
- テスト中にデータベースとキャッシュを監視する — DB の接続プール枯渇、Redis のエビクション、ロック待機時間を確認してください。アプリケーション層のスループットに問題がなく見えても、データベースが過負荷になっていることがあります
- テスト注文にタグを付けて簡単にクリーンアップできるようにする — テスト実行後にテストデータを一括削除できるよう、一貫したテスト用メールドメイン(
@test-load.invalid)を使用してください:DELETE FROM orders WHERE email LIKE '%@test-load.invalid'
よくある落とし穴
| 問題 | 解決策 |
|---|---|
| 商品が売り切れているため、チェックアウトのシナリオが失敗する | 在庫数無制限のテスト商品を確保します。専用の is_load_test_product フラグを使用し、実際の商品カタログページでは除外します |
| Loader.io のテストには合格するが、本番環境では依然として遅くなる | ステージング環境の負荷が本番環境と一致していない可能性があります。Redis Object Cache が有効であること、MySQL のバージョンが一致していること、サーバーのサイズが同じであることを確認します |
| 目標 RPS に到達する前に k6 の VU を使い切る | ramping-vus(同時ユーザー数を制御)ではなく、ramping-arrival-rate エグゼキューター(RPS を制御)を使用します。preAllocatedVUs と maxVUs の値を増やします |
| 計画済みの負荷テスト中にアラートが頻発する | 監視システムに負荷テスト用フラグ(Datadog のタグ、Grafana のアノテーション)を追加し、予定されたテスト期間中は重要度の低いアラートを抑制します |
| 実行ごとに結果を再現できない | Math.random() による商品生成ではなく、固定のデータセットファイルを使用した SharedArray を使います。実行前にテストデータセットを固定します |
関連スキル
- @flash-sale-scaling
- @monitoring-alerting-commerce
- @database-optimization-commerce
- @bot-protection