效能最佳化
在 Shopify 生態系統中,效能不是可選的。Shopify 在應用程式審核期間強制執行嚴格的效能要求,商家會卸載那些拖慢其店面的應用程式。在本模組中,您將學習如何從設計層面建構高效的應用程式、監控真實世界的效能指標,以及優化應用程式堆疊的每一層。
Web Vitals 監控
Google 的 Core Web Vitals 是衡量使用者感知效能的行業標準。Shopify 會追蹤店面的這些指標,並對降低這些指標的應用程式進行懲罰。
三個核心 Web Vitals
| 指標 | 衡量內容 | 良好門檻 |
|---|---|---|
| LCP(最大內容繪製) | 載入效能 | < 2.5 秒 |
| INP(與下次繪製的互動) | 互動響應性 | < 200 毫秒 |
| CLS(累積版面位移) | 視覺穩定性 | < 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。
效能預算
為您的應用程式的店面影響建立效能預算:
// 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>
);
}
元件級延遲載入
對於頁面中的重型元件,使用交叉觀察器模式:
// 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 驅動的快取失效是黃金標準——當您收到相關的 webhook 如 products/update 時使快取資料失效。
資料庫查詢最佳化
索引策略
您的應用程式資料庫通常是主要的瓶頸。務必為 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、錯誤追蹤、告警 | 持續進行 |
效能最佳化是一個持續的紀律,而不是一次性的任務。從第一天起就在應用程式中建立監控,設定效能預算,並將效能退化視為阻擋發布的 bug。商家依賴快速的店面來獲取收入,您的應用程式必須尊重這一約束。