Skip to main content

性能优化

在 Shopify 生态系统中,性能不是可选项。Shopify 在应用审核期间执行严格的性能要求,商家会卸载拖慢其店面速度的应用。在本模块中,你将学习如何在设计上构建快速的应用,监控真实世界的性能指标,并优化应用堆栈的每一层。

Web Vitals 监控

Google 的 Core Web Vitals 是衡量用户感知性能的行业标准。Shopify 跟踪店面的这些指标,并对降低这些指标的应用进行处罚。

三个核心 Web Vitals

指标衡量内容良好阈值
LCP(Largest Contentful Paint)加载性能< 2.5 秒
INP(Interaction to Next Paint)交互响应性< 200 毫秒
CLS(Cumulative Layout Shift)视觉稳定性< 0.1
区域性能数据(Winter '26)

从 Winter '26 版本开始,Shopify 在合作伙伴仪表板中提供 区域 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. 对店面影响最小 -- 你的应用应增加不超过 100 毫秒的页面加载时间
  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 在合作伙伴仪表板中提供了一个 店面性能仪表板,它准确显示你的应用如何影响安装了该应用的商店的 Web Vitals。在发布后持续监控此仪表板,而不仅仅是在应用审核之前。

优化检查清单总结

领域操作优先级
JavaScript异步加载、代码分割、Tree Shaking关键
图片Shopify CDN 转换、延迟加载、WebP关键
API 调用缓存、批处理、批量操作
数据库索引、连接池、查询优化
CSS内联关键 CSS、异步加载样式表
字体font-display: swap、预加载关键字体
监控RUM、错误跟踪、告警持续

性能优化是一门持续的学科,而不是一次性任务。从第一天起就在你的应用中构建监控,设置性能预算,并将回退视为阻止发布的 Bug。商家依赖快速的店面来获得收入,你的应用必须尊重这一约束。