パフォーマンス最適化
Shopify エコシステムにおいてパフォーマンスはオプションではありません。Shopify はアプリレビュー中に厳格なパフォーマンス要件を強制し、マーチャントはストアフロントを遅くするアプリをアンインストールします。このモジュールでは、設計段階から高速なアプリの構築方法、実世界のパフォーマンスメトリクスの監視、アプリケーションスタックのすべてのレイヤーの最適化を学びます。
Web Vitals モニタリング
Google の Core Web Vitals は、ユーザーが感じるパフォーマンスを測定するための業界標準です。Shopify はストアフロントのこれらのメトリクスを追跡し、それらを低下させるアプリにペナルティを課します。
3つのコア Web Vitals
| メトリクス | 測定内容 | 良好なしきい値 |
|---|---|---|
| LCP(Largest Contentful Paint) | 読み込みパフォーマンス | < 2.5 秒 |
| INP(Interaction to Next Paint) | インタラクティビティの応答性 | < 200 ミリ秒 |
| CLS(Cumulative Layout Shift) | 視覚的安定性 | < 0.1 |
Winter '26 リリースから、Shopify は Partner Dashboard で地域別 Web Vitals データを提供しています。アプリが異なる地理的地域でどのようなパフォーマンスを示しているかを確認でき、CDN カバレッジのギャップや特定のデータセンターでの遅い API エンドポイントなどの地域固有のボトルネックを特定・修正できます。
Web Vitals トラッキングの実装
アプリのストアフロントコンポーネントにリアルユーザーモニタリング(RUM)を追加します:
// web-vitals-monitor.js
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
shopDomain: window.Shopify?.shop,
});
// Use sendBeacon for reliability during page unload
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/analytics/vitals', body);
} else {
fetch('/api/analytics/vitals', {
body,
method: 'POST',
keepalive: true,
});
}
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
マーチャントのストアフロントに同期 JavaScript を絶対に注入しないでください。すべてのストアフロントスクリプトは defer または async 属性を使用して非同期にロードするか、Shopify の Script Tag API を使用して display_scope を適切に設定してロードする必要があります。
パフォーマンス Budgets
アプリのストアフロント影響に対するパフォーマンスバジェットを設定します:
// performance-budget.config.js
export default {
budgets: [
{
path: '/apps/your-app/*',
resourceSizes: [
{ resourceType: 'script', budget: 50 }, // 50 KB max JS
{ resourceType: 'stylesheet', budget: 20 }, // 20 KB max CSS
{ resourceType: 'image', budget: 100 }, // 100 KB max images
{ resourceType: 'total', budget: 200 }, // 200 KB total
],
resourceCounts: [
{ resourceType: 'third-party', budget: 3 }, // Max 3 third-party requests
],
},
],
};
遅延読み込みとコード分割
Shopify アプリは複雑な管理インターフェースを含むことが多くあります。すべてを事前にロードすると帯域幅を浪費し、初期レンダリングが遅くなります。
React によるルートベースのコード分割
// app/routes.jsx
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
import { SkeletonPage } from '@shopify/polaris';
// Lazy load heavy route components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const ProductSync = lazy(() => import('./pages/ProductSync'));
const Analytics = lazy(() => import('./pages/Analytics'));
const BulkEditor = lazy(() => import('./pages/BulkEditor'));
function AppRoutes() {
return (
<Suspense fallback={<SkeletonPage primaryAction />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/sync" element={<ProductSync />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/bulk-edit" element={<BulkEditor />} />
</Routes>
</Suspense>
);
}
コンポーネントレベルの遅延読み込み
ページ内の重いコンポーネントには、Intersection Observer パターンを使用します:
// components/LazyChart.jsx
import { useEffect, useRef, useState } from 'react';
export function LazyChart({ data }) {
const containerRef = useRef(null);
const [ChartComponent, setChartComponent] = useState(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
import('./HeavyChartLibrary').then((mod) => {
setChartComponent(() => mod.Chart);
});
observer.disconnect();
}
},
{ rootMargin: '200px' } // Start loading 200px before visible
);
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={containerRef} style={{ minHeight: 300 }}>
{ChartComponent ? <ChartComponent data={data} /> : <SkeletonChart />}
</div>
);
}
ストアフロントコンポーネントの画像最適化
アプリがストアフロントで画像をレンダリングする場合、常に Shopify の画像 CDN 変換を使用してください:
{% comment %} Use Shopify's image_url filter for automatic CDN optimization {% endcomment %}
{% assign optimized_url = image | image_url: width: 400, height: 400, crop: 'center' %}
<img
src="{{ optimized_url }}"
loading="lazy"
decoding="async"
width="400"
height="400"
alt="{{ image.alt | escape }}"
/>
CDN とキャッシュ戦略
HTTP キャッシュヘッダー
アプリサーバーが適切なキャッシュヘッダーを返すように設定します:
// middleware/caching.js
export function cachingMiddleware(req, res, next) {
const path = req.path;
if (path.startsWith('/assets/')) {
// Static assets: cache for 1 year with immutable
res.set('Cache-Control', 'public, max-age=31536000, immutable');
} else if (path.startsWith('/api/storefront/')) {
// Storefront API responses: short cache with stale-while-revalidate
res.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=300');
} else if (path.startsWith('/api/admin/')) {
// Admin API responses: no cache (contains merchant-specific data)
res.set('Cache-Control', 'private, no-cache, no-store');
}
next();
}
GraphQL レスポンスの Redis キャッシュ
頻繁にアクセスされる Shopify API データをキャッシュしてレイテンシーと API レート制限の消費を削減します:
// services/cached-shopify-client.js
import { createClient } from 'redis';
import crypto from 'crypto';
const redis = createClient({ url: process.env.REDIS_URL });
export async function cachedGraphQLQuery(session, query, variables, ttl = 300) {
const cacheKey = `shopify:${session.shop}:${crypto
.createHash('md5')
.update(JSON.stringify({ query, variables }))
.digest('hex')}`;
// Check cache first
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Fetch from Shopify
const client = new shopify.clients.Graphql({ session });
const response = await client.query({ data: { query, variables } });
// Cache the response
await redis.setEx(cacheKey, ttl, JSON.stringify(response.body));
return response.body;
}
データの変動性に基づいて異なる TTL 値を使用してください。商品データは 5 分間キャッシュできますが、在庫レベルの TTL は 30 秒以下にする必要があります。Webhook 駆動のキャッシュ無効化がゴールドスタンダードです——products/update などの関連する Webhook を受信したときにキャッシュデータを無効化してください。
データベースクエリ最適化
インデックス戦略
アプリのデータベースは主要なボトルネックになることが多いです。WHERE 句と JOIN 条件で使用されるカラムには常にインデックスを作成してください:
-- Essential indexes for a typical Shopify app
CREATE INDEX idx_shops_domain ON shops(shopify_domain);
CREATE INDEX idx_sessions_shop_id ON sessions(shop_id);
CREATE INDEX idx_sync_jobs_shop_status ON sync_jobs(shop_id, status);
CREATE INDEX idx_products_shop_shopify_id ON products(shop_id, shopify_product_id);
-- Composite index for common query patterns
CREATE INDEX idx_orders_shop_date ON orders(shop_id, created_at DESC);
Prisma による N+1 クエリの防止
// BAD: N+1 queries
const shops = await prisma.shop.findMany();
for (const shop of shops) {
// This runs a separate query for EACH shop
const products = await prisma.product.findMany({
where: { shopId: shop.id },
});
}
// GOOD: Eager loading with include
const shops = await prisma.shop.findMany({
include: {
products: {
where: { syncStatus: 'active' },
take: 100,
},
},
});
コネクションプーリング
ホスティング環境に基づいてデータベースコネクションプールを設定します:
// prisma/schema.prisma connection string with pooling
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
// For serverless environments (Vercel, AWS Lambda)
// Use PgBouncer or Prisma Accelerate for connection pooling
}
サーバーレス環境はコールドスタートのたびに新しいデータベース接続を作成します。コネクションプーリングがないと、トラフィックスパイク時にデータベースの接続制限を使い果たす可能性があります。サーバーレスプラットフォームにデプロイする際は、常に PgBouncer、Supabase の組み込みプーラー、または Prisma Accelerate などの外部コネクションプーラーを使用してください。
アプリレビューにおける Shopify のパフォーマンス要件
Shopify のアプリレビューチームはパフォーマンスを合格/不合格の基準として評価します。これらの要件を満たさない場合、アプリは却下されます。
主要な要件
- 同期スクリプト注入の禁止 -- すべての JavaScript は非同期でロードする必要があります
- ストアフロントへの影響を最小限に -- アプリはページロード時間に 100ms 未満しか追加してはいけません
- 効率的な API 使用 -- レート制限の範囲内に余裕を持つ;大規模データセットにはバルクオペレーションを使用
- レイアウトシフトなし -- 動的にロードされるコンテンツ用にスペースを予約
- 高速な管理画面ロード -- Shopify 管理画面内のアプリページは 3 秒以内にロード
申請前パフォーマンスチェックリスト
# Run Lighthouse audit against your app's storefront components
npx lighthouse https://test-store.myshopify.com \
--only-categories=performance \
--output=json \
--output-path=./lighthouse-report.json
# Check bundle sizes
npx webpack-bundle-analyzer stats.json
# Profile API calls
shopify app dev --verbose 2>&1 | grep "GraphQL"
Shopify は Partner Dashboard でストアフロントパフォーマンスダッシュボードを提供しており、アプリがインストールされているストアの Web Vitals にどのような影響を与えているかを正確に表示します。アプリレビュー前だけでなく、ローンチ後も継続的にこのダッシュボードを監視してください。
最適化チェックリストまとめ
| エリア | アクション | 優先度 |
|---|---|---|
| JavaScript | 非同期ロード、コード分割、ツリーシェイキング | 最重要 |
| 画像 | Shopify CDN 変換、遅延読み込み、WebP | 最重要 |
| API 呼び出し | キャッシュ、バッチ処理、バルクオペレーション | 高 |
| データベース | インデックス、コネクションプーリング、クエリ最適化 | 高 |
| CSS | クリティカル CSS のインライン化、非同期スタイルシートロード | 中 |
| フォント | font-display: swap、クリティカルフォントのプリロード | 中 |
| モニタリング | RUM、エラー追跡、アラート | 継続的 |
パフォーマンス最適化は一回限りのタスクではなく、継続的な規律です。初日からアプリにモニタリングを組み込み、パフォーマンスバジェットを設定し、リグレッションをリリースをブロックするバグとして扱いましょう。マーチャントは収益のために高速なストアフロントに依存しており、アプリはその制約を尊重する必要があります。