Skip to main content

效能最佳化

在 Shopify 生態系統中,效能不是可選的。Shopify 在應用程式審核期間強制執行嚴格的效能要求,商家會卸載那些拖慢其店面的應用程式。在本模組中,您將學習如何從設計層面建構高效的應用程式、監控真實世界的效能指標,以及優化應用程式堆疊的每一層。

Web Vitals 監控

Google 的 Core Web Vitals 是衡量使用者感知效能的行業標準。Shopify 會追蹤店面的這些指標,並對降低這些指標的應用程式進行懲罰。

三個核心 Web Vitals

指標衡量內容良好門檻
LCP(最大內容繪製)載入效能< 2.5 秒
INP(與下次繪製的互動)互動響應性< 200 毫秒
CLS(累積版面位移)視覺穩定性< 0.1
區域效能資料(Winter '26)

從 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);
warning

絕不要將同步 JavaScript 注入商家的店面。所有店面腳本都必須使用 deferasync 屬性非同步載入,或透過 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;
}
tip

根據資料的變動性使用不同的 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
}
danger

無伺服器環境在每次冷啟動時都會建立新的資料庫連線。沒有連線池的話,流量尖峰時可能會耗盡資料庫的連線限制。在部署到無伺服器平台時,務必使用外部連線池如 PgBouncer、Supabase 的內建連線池或 Prisma Accelerate。

Shopify 的應用程式審核效能要求

Shopify 的應用程式審核團隊將效能評估為通過/不通過的標準。不符合這些要求將導致您的應用程式被拒絕。

核心要求

  1. 禁止同步腳本注入 —— 所有 JavaScript 都必須非同步載入
  2. 最小化店面影響 —— 您的應用程式應增加不到 100ms 的頁面載入時間
  3. 高效的 API 使用 —— 保持在速率限制之內;對大型資料集使用批量操作
  4. 無版面位移 —— 為動態載入的內容預留空間
  5. 快速的管理後台載入 —— 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"
info

Shopify 在 Partner Dashboard 中提供店面效能儀表板,精確顯示您的應用程式如何影響已安裝商店的 Web Vitals。在發布後持續監控此儀表板,而不僅僅是在應用程式審核之前。

最佳化檢查清單摘要

領域操作優先級
JavaScript非同步載入、程式碼分割、樹搖優化關鍵
圖片Shopify CDN 轉換、延遲載入、WebP關鍵
API 呼叫快取、批次處理、批量操作
資料庫索引、連線池、查詢最佳化
CSS關鍵 CSS 內聯、非同步樣式表載入
字型font-display: swap、預載入關鍵字型
監控RUM、錯誤追蹤、告警持續進行

效能最佳化是一個持續的紀律,而不是一次性的任務。從第一天起就在應用程式中建立監控,設定效能預算,並將效能退化視為阻擋發布的 bug。商家依賴快速的店面來獲取收入,您的應用程式必須尊重這一約束。