performance-optimization

작성자: sanity-io

애플리케이션 성능을 최적화합니다. 성능 요구사항이 있거나, 성능 회귀가 의심되거나, Core Web Vitals 또는 로드 시간이…

npx skills add https://github.com/sanity-io/sanity --skill performance-optimization

Performance Optimization

Overview

Measure before optimizing. Performance work without measurement is guessing — and guessing leads to premature optimization that adds complexity without improving what matters. Profile first, identify the actual bottleneck, fix it, measure again. Optimize only what measurements prove matters.

When to Use

  • Performance requirements exist in the spec (load time budgets, response time SLAs)
  • Users or monitoring report slow behavior
  • Core Web Vitals scores are below thresholds
  • You suspect a change introduced a regression
  • Building features that handle large datasets or high traffic

When NOT to use: Don't optimize before you have evidence of a problem. Premature optimization adds complexity that costs more than the performance it gains.

Core Web Vitals Targets

MetricGoodNeeds ImprovementPoor
LCP (Largest Contentful Paint)≤ 2.5s≤ 4.0s> 4.0s
INP (Interaction to Next Paint)≤ 200ms≤ 500ms> 500ms
CLS (Cumulative Layout Shift)≤ 0.1≤ 0.25> 0.25

The Optimization Workflow

1. MEASURE  → Establish baseline with real data
2. IDENTIFY → Find the actual bottleneck (not assumed)
3. FIX      → Address the specific bottleneck
4. VERIFY   → Measure again, confirm improvement
5. GUARD    → Add monitoring or tests to prevent regression

Step 1: Measure

Two complementary approaches — use both:

  • Synthetic (Lighthouse, DevTools Performance tab): Controlled conditions, reproducible. Best for CI regression detection and isolating specific issues.
  • RUM (web-vitals library, CrUX): Real user data in real conditions. Required to validate that a fix actually improved user experience.

Frontend:

# Synthetic: Lighthouse in Chrome DevTools (or CI)
# Chrome DevTools → Performance tab → Record
# Chrome DevTools MCP → Performance trace

# RUM: Web Vitals library in code
import { onLCP, onINP, onCLS } from 'web-vitals';

onLCP(console.log);
onINP(console.log);
onCLS(console.log);

Backend:

# Response time logging
# Application Performance Monitoring (APM)
# Database query logging with timing

# Simple timing
console.time('db-query');
const result = await db.query(...);
console.timeEnd('db-query');

Where to Start Measuring

Use the symptom to decide what to measure first:

What is slow?
├── First page load
│   ├── Large bundle? --> Measure bundle size, check code splitting
│   ├── Slow server response? --> Measure TTFB in DevTools Network waterfall
│   │   ├── DNS long? --> Add dns-prefetch / preconnect for known origins
│   │   ├── TCP/TLS long? --> Enable HTTP/2, check edge deployment, keep-alive
│   │   └── Waiting (server) long? --> Profile backend, check queries and caching
│   └── Render-blocking resources? --> Check network waterfall for CSS/JS blocking
├── Interaction feels sluggish
│   ├── UI freezes on click? --> Profile main thread, look for long tasks (>50ms)
│   ├── Form input lag? --> Check re-renders, controlled component overhead
│   └── Animation jank? --> Check layout thrashing, forced reflows
├── Page after navigation
│   ├── Data loading? --> Measure API response times, check for waterfalls
│   └── Client rendering? --> Profile component render time, check for N+1 fetches
└── Backend / API
    ├── Single endpoint slow? --> Profile database queries, check indexes
    ├── All endpoints slow? --> Check connection pool, memory, CPU
    └── Intermittent slowness? --> Check for lock contention, GC pauses, external deps

Step 2: Identify the Bottleneck

Common bottlenecks by category:

Frontend:

SymptomLikely CauseInvestigation
Slow LCPLarge images, render-blocking resources, slow serverCheck network waterfall, image sizes
High CLSImages without dimensions, late-loading content, font shiftsCheck layout shift attribution
Poor INPHeavy JavaScript on main thread, large DOM updatesCheck long tasks in Performance trace
Slow initial loadLarge bundle, many network requestsCheck bundle size, code splitting

Backend:

SymptomLikely CauseInvestigation
Slow API responsesN+1 queries, missing indexes, unoptimized queriesCheck database query log
Memory growthLeaked references, unbounded caches, large payloadsHeap snapshot analysis
CPU spikesSynchronous heavy computation, regex backtrackingCPU profiling
High latencyMissing caching, redundant computation, network hopsTrace requests through the stack

Step 3: Fix Common Anti-Patterns

N+1 Queries (Backend)

// BAD: N+1 — one query per task for the owner
const tasks = await db.tasks.findMany()
for (const task of tasks) {
  task.owner = await db.users.findUnique({where: {id: task.ownerId}})
}

// GOOD: Single query with join/include
const tasks = await db.tasks.findMany({
  include: {owner: true},
})

Unbounded Data Fetching

// BAD: Fetching all records
const allTasks = await db.tasks.findMany()

// GOOD: Paginated with limits
const tasks = await db.tasks.findMany({
  take: 20,
  skip: (page - 1) * 20,
  orderBy: {createdAt: 'desc'},
})

Missing Image Optimization (Frontend)

<!-- BAD: No dimensions, no format optimization -->
<img src="/hero.jpg" />

<!-- GOOD: Hero / LCP image — art direction + resolution switching, high priority -->
<!--
  Two techniques combined:
  - Art direction (media): different crop/composition per breakpoint
  - Resolution switching (srcset + sizes): right file size per screen density
-->
<picture>
  <!-- Mobile: portrait crop (8:10) -->
  <source
    media="(max-width: 767px)"
    srcset="/hero-mobile-400.avif 400w, /hero-mobile-800.avif 800w"
    sizes="100vw"
    width="800"
    height="1000"
    type="image/avif"
  />
  <source
    media="(max-width: 767px)"
    srcset="/hero-mobile-400.webp 400w, /hero-mobile-800.webp 800w"
    sizes="100vw"
    width="800"
    height="1000"
    type="image/webp"
  />
  <!-- Desktop: landscape crop (2:1) -->
  <source
    srcset="/hero-800.avif 800w, /hero-1200.avif 1200w, /hero-1600.avif 1600w"
    sizes="(max-width: 1200px) 100vw, 1200px"
    width="1200"
    height="600"
    type="image/avif"
  />
  <source
    srcset="/hero-800.webp 800w, /hero-1200.webp 1200w, /hero-1600.webp 1600w"
    sizes="(max-width: 1200px) 100vw, 1200px"
    width="1200"
    height="600"
    type="image/webp"
  />
  <img
    src="/hero-desktop.jpg"
    width="1200"
    height="600"
    fetchpriority="high"
    alt="Hero image description"
  />
</picture>

<!-- GOOD: Below-the-fold image — lazy loaded + async decoding -->
<img
  src="/content.webp"
  width="800"
  height="400"
  loading="lazy"
  decoding="async"
  alt="Content image description"
/>

Unnecessary Re-renders (React)

// BAD: Creates new object on every render, causing children to re-render
function TaskList() {
  return <TaskFilters options={{sortBy: 'date', order: 'desc'}} />
}

// GOOD: Stable reference
const DEFAULT_OPTIONS = {sortBy: 'date', order: 'desc'} as const
function TaskList() {
  return <TaskFilters options={DEFAULT_OPTIONS} />
}

// Use React.memo for expensive components
const TaskItem = React.memo(function TaskItem({task}: Props) {
  return <div>{/* expensive render */}</div>
})

// Use useMemo for expensive computations
function TaskStats({tasks}: Props) {
  const stats = useMemo(() => calculateStats(tasks), [tasks])
  return (
    <div>
      {stats.completed} / {stats.total}
    </div>
  )
}

Large Bundle Size

// Modern bundlers (Vite, webpack 5+) handle named imports with tree-shaking automatically,
// provided the dependency ships ESM and is marked `sideEffects: false` in package.json.
// Profile before changing import styles — the real gains come from splitting and lazy loading.

// GOOD: Dynamic import for heavy, rarely-used features
const ChartLibrary = lazy(() => import('./ChartLibrary'));

// GOOD: Route-level code splitting wrapped in Suspense
const SettingsPage = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <SettingsPage />
    </Suspense>
  );
}

Missing Caching (Backend)

// Cache frequently-read, rarely-changed data
const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
let cachedConfig: AppConfig | null = null
let cacheExpiry = 0

async function getAppConfig(): Promise<AppConfig> {
  if (cachedConfig && Date.now() < cacheExpiry) {
    return cachedConfig
  }
  cachedConfig = await db.config.findFirst()
  cacheExpiry = Date.now() + CACHE_TTL
  return cachedConfig
}

// HTTP caching headers for static assets
app.use(
  '/static',
  express.static('public', {
    maxAge: '1y', // Cache for 1 year
    immutable: true, // Never revalidate (use content hashing in filenames)
  }),
)

// Cache-Control for API responses
res.set('Cache-Control', 'public, max-age=300') // 5 minutes

Performance Budget

Set budgets and enforce them:

JavaScript bundle: < 200KB gzipped (initial load)
CSS: < 50KB gzipped
Images: < 200KB per image (above the fold)
Fonts: < 100KB total
API response time: < 200ms (p95)
Time to Interactive: < 3.5s on 4G
Lighthouse Performance score: ≥ 90

Enforce in CI:

# Bundle size check
npx bundlesize --config bundlesize.config.json

# Lighthouse CI
npx lhci autorun

See Also

For detailed performance checklists, optimization commands, and anti-pattern reference, see references/performance-checklist.md.

Common Rationalizations

RationalizationReality
"We'll optimize later"Performance debt compounds. Fix obvious anti-patterns now, defer micro-optimizations.
"It's fast on my machine"Your machine isn't the user's. Profile on representative hardware and networks.
"This optimization is obvious"If you didn't measure, you don't know. Profile first.
"Users won't notice 100ms"Research shows 100ms delays impact conversion rates. Users notice more than you think.
"The framework handles performance"Frameworks prevent some issues but can't fix N+1 queries or oversized bundles.

Red Flags

  • Optimization without profiling data to justify it
  • N+1 query patterns in data fetching
  • List endpoints without pagination
  • Images without dimensions, lazy loading, or responsive sizes
  • Bundle size growing without review
  • No performance monitoring in production
  • React.memo and useMemo everywhere (overusing is as bad as underusing)

Verification

After any performance-related change:

  • Before and after measurements exist (specific numbers)
  • The specific bottleneck is identified and addressed
  • Core Web Vitals are within "Good" thresholds
  • Bundle size hasn't increased significantly
  • No N+1 queries in new data fetching code
  • Performance budget passes in CI (if configured)
  • Existing tests still pass (optimization didn't break behavior)

sanity-io의 다른 스킬

tdd
sanity-io
레드-그린-리팩터 루프를 사용한 테스트 주도 개발. 사용자가 TDD로 기능을 구축하거나 버그를 수정하려 할 때, "레드-그린-리팩터"를 언급하거나, 원할 때 사용합니다...
content-experimentation-best-practices
sanity-io
콘텐츠 실험을 설계, 실행, 분석하여 전환율과 참여도를 개선하기 위한 체계적인 가이드입니다. 가설 프레임워크, 지표 선택, 표본 크기 계산, A/B 및 다변량 실험의 통계적 유의성 검정을 다룹니다. p-값, 신뢰 구간, 검정력 분석, 결과 해석을 위한 베이지안 방법에 대한 상세 자료를 포함합니다. 필드 수준에서 변형을 관리하고 외부 시스템과 연결하기 위한 CMS 통합 패턴을 제공합니다.
content-modeling-best-practices
sanity-io
구조화된 콘텐츠 모델링 가이드로, 스키마 설계, 재사용성, 멀티채널 전달을 다룹니다. 콘텐츠를 페이지가 아닌 데이터로 취급하고, 단일 진실 공급원을 유지하며, 미래 채널을 고려한 설계와 편집자 워크플로우 최적화를 위한 핵심 원칙을 포함합니다. 참조와 임베디드 객체 간의 결정 프레임워크, 관심사 분리, 콘텐츠 재사용 패턴을 제공하며, 플랫, 계층적, 패싯 접근 방식에 대한 분류 및 분류 체계 가이드를 포함합니다. 다음에 적용됩니다...
portable-text-conversion
sanity-io
HTML 및 Markdown 콘텐츠를 Sanity용 Portable Text 블록으로 변환합니다. 레거시 CMS에서 콘텐츠를 마이그레이션하거나 HTML 또는 Markdown을 Sanity로 가져올 때 사용합니다.
portable-text-serialization
sanity-io
Portable Text를 React, Svelte, Vue, Astro, HTML, Markdown 및 일반 텍스트로 렌더링하고 직렬화합니다. 모든 프론트엔드에서 Portable Text 렌더링을 구현할 때 사용합니다…
sanity-best-practices
sanity-io
Sanity CMS 개발을 위한 포괄적인 모범 사례 및 통합 가이드로, 다양한 프레임워크와 주제를 다룹니다. Next.js, Nuxt, Astro, Remix, SvelteKit, Angular 등 10개 이상의 프레임워크 통합을 프레임워크별 패턴 및 설정 가이드와 함께 포함합니다. 스키마 디자인, GROQ 쿼리 최적화, Visual Editing, Portable Text, 이미지, TypeGen, 현지화, 콘텐츠 마이그레이션을 위한 주제 가이드를 제공합니다. 작업 유형에 따라 관련 가이드만 로드할 수 있는 빠른 참조 구조를 제공합니다.
sanity-migration
sanity-io
다른 CMS 및 콘텐츠 시스템에서 Sanity로의 마이그레이션을 계획, 구현 및 검토합니다. AEM, Adobe Experience Manager, Contentful, Strapi, Webflow, WordPress, Payload, Drupal, Markdown/MDX/frontmatter 파일, WXR/XML 내보내기, CMS API, 데이터베이스 덤프, 정적 HTML에서 Sanity로 마이그레이션하거나 리플랫폼할 때, 또는 추출, 변환, Portable Text 변환, 에셋 마이그레이션, 리디렉션, 검증 및 전환 워크플로를 설계할 때 사용합니다.
data-analysisdatabasedevelopment
seo-aeo-best-practices
sanity-io
포괄적인 SEO 및 AEO 최적화 가이드로, 메타데이터, 구조화된 데이터, EEAT, AI 대응성을 다룹니다. Google, ChatGPT, Perplexity 및 유사 플랫폼에서 전통적인 검색 엔진 최적화(SEO)와 AI 답변 엔진 최적화(AEO)를 모두 포함합니다. 기술적 SEO 필수 요소인 메타데이터, Open Graph 태그, 사이트맵, robots.txt, hreflang 및 국제 SEO 구성을 제공합니다. Article, FAQ, Breadcrumb 및 Product 스키마에 대한 JSON-LD 구조화된 데이터 패턴을 제공합니다. 구현합니다...