{"id":"performance","name":"performance","summary":"ウェブパフォーマンスを最適化し、より高速な読み込みとより良いユーザー体験を実現します。「サイトのスピードアップ」「パフォーマンス最適化」「ロード時間短縮」「読み込みの遅さの修正」「ページ速度の改善」「パフォーマンス監査」などを求められたときに使ってください。","body":"# Performance optimization\n\nDeep performance optimization based on Lighthouse performance audits. Focuses on loading speed, runtime efficiency, and resource optimization.\n\n## How it works\n\n1. Identify performance bottlenecks in code and assets\n2. Prioritize by impact on Core Web Vitals\n3. Provide specific optimizations with code examples\n4. Measure improvement with before/after metrics\n\n## Performance budget\n\n| Resource | Budget | Rationale |\n|----------|--------|-----------|\n| Total page weight | < 1.5 MB | 3G loads in ~4s |\n| JavaScript (compressed) | < 300 KB | Parsing + execution time |\n| CSS (compressed) | < 100 KB | Render blocking |\n| Images (above-fold) | < 500 KB | LCP impact |\n| Fonts | < 100 KB | FOIT/FOUT prevention |\n| Third-party | < 200 KB | Uncontrolled latency |\n\n## Critical rendering path\n\n### Server response\n* **TTFB < 800ms.** Time to First Byte should be fast. Use CDN, caching, and efficient backends.\n* **Enable compression.** Gzip or Brotli for text assets. Brotli preferred (15-20% smaller).\n* **HTTP/2 or HTTP/3.** Multiplexing reduces connection overhead.\n* **Edge caching.** Cache HTML at CDN edge when possible.\n* **Send Early Hints (HTTP 103) for slow origins.** When the origin needs hundreds of milliseconds to assemble the final response, return a `103 Early Hints` with `Link: </hero.webp>; rel=preload; as=image` (and similar for critical CSS/fonts) so the browser starts fetching before the `200 OK` lands. Cloudflare reports [20–30% LCP improvements](https://blog.cloudflare.com/early-hints-performance/) on image-heavy pages. Requires HTTP/2+ and is supported by Chromium-based browsers; other browsers ignore the 103 and fall through to the 200 — safe to enable. CDNs (Cloudflare, Fastly, Akamai) can synthesize 103s automatically from prior responses; on your own origin, emit them from the same handler that issues the 200.\n\n### Resource loading\n\n**Preconnect to required origins:**\n```html\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://cdn.example.com\" crossorigin>\n```\n\n**Preload critical resources:**\n```html\n<!-- LCP image -->\n<link rel=\"preload\" href=\"/hero.webp\" as=\"image\" fetchpriority=\"high\">\n\n<!-- Critical font -->\n<link rel=\"preload\" href=\"/font.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>\n```\n\n**Prerender likely-next navigations** with the [Speculation Rules API](https://developer.chrome.com/docs/web-platform/prerender-pages):\n```html\n<script type=\"speculationrules\">\n{\n  \"prerender\": [{\n    \"where\": { \"href_matches\": \"/*\" },\n    \"eagerness\": \"moderate\"\n  }]\n}\n</script>\n```\n`moderate` triggers after a ~200ms hover — usually intent-correlated, rarely wasted. See [core-web-vitals → LCP](../core-web-vitals/SKILL.md#lcp-largest-contentful-paint) for the full discussion of eagerness tradeoffs and the `prerenderingchange` gating you'll need for analytics.\n\n**Defer non-critical CSS:**\n```html\n<!-- Critical CSS inlined -->\n<style>/* Above-fold styles */</style>\n\n<!-- Non-critical CSS -->\n<link rel=\"preload\" href=\"/styles.css\" as=\"style\" onload=\"this.onload=null;this.rel='stylesheet'\">\n<noscript><link rel=\"stylesheet\" href=\"/styles.css\"></noscript>\n```\n\n### JavaScript optimization\n\n**Defer non-essential scripts:**\n```html\n<!-- Parser-blocking (avoid) -->\n<script src=\"/critical.js\"></script>\n\n<!-- Deferred (preferred) -->\n<script defer src=\"/app.js\"></script>\n\n<!-- Async (for independent scripts) -->\n<script async src=\"/analytics.js\"></script>\n\n<!-- Module (deferred by default) -->\n<script type=\"module\" src=\"/app.mjs\"></script>\n```\n\n**Code splitting patterns:**\n```javascript\n// Route-based splitting\nconst Dashboard = lazy(() => import('./Dashboard'));\n\n// Component-based splitting\nconst HeavyChart = lazy(() => import('./HeavyChart'));\n\n// Feature-based splitting\nif (user.isPremium) {\n  const PremiumFeatures = await import('./PremiumFeatures');\n}\n```\n\n**Tree shaking best practices:**\n```javascript\n// ❌ Imports entire library\nimport _ from 'lodash';\n_.debounce(fn, 300);\n\n// ✅ Imports only what's needed\nimport debounce from 'lodash/debounce';\ndebounce(fn, 300);\n```\n\n## Image optimization\n\n### Format selection\n| Format | Use case | Browser support |\n|--------|----------|-----------------|\n| AVIF | Photos, best compression | 92%+ |\n| WebP | Photos, good fallback | 97%+ |\n| PNG | Graphics with transparency | Universal |\n| SVG | Icons, logos, illustrations | Universal |\n\n### Responsive images\n```html\n<picture>\n  <!-- AVIF for modern browsers -->\n  <source \n    type=\"image/avif\"\n    srcset=\"hero-400.avif 400w,\n            hero-800.avif 800w,\n            hero-1200.avif 1200w\"\n    sizes=\"(max-width: 600px) 100vw, 50vw\">\n  \n  <!-- WebP fallback -->\n  <source \n    type=\"image/webp\"\n    srcset=\"hero-400.webp 400w,\n            hero-800.webp 800w,\n            hero-1200.webp 1200w\"\n    sizes=\"(max-width: 600px) 100vw, 50vw\">\n  \n  <!-- JPEG fallback -->\n  <img \n    src=\"hero-800.jpg\"\n    srcset=\"hero-400.jpg 400w,\n            hero-800.jpg 800w,\n            hero-1200.jpg 1200w\"\n    sizes=\"(max-width: 600px) 100vw, 50vw\"\n    width=\"1200\" \n    height=\"600\"\n    alt=\"Hero image\"\n    loading=\"lazy\"\n    decoding=\"async\">\n</picture>\n```\n\n### LCP image priority\n```html\n<!-- Above-fold LCP image: eager loading, high priority -->\n<img \n  src=\"hero.webp\" \n  fetchpriority=\"high\"\n  loading=\"eager\"\n  decoding=\"sync\"\n  alt=\"Hero\">\n\n<!-- Below-fold images: lazy loading -->\n<img \n  src=\"product.webp\" \n  loading=\"lazy\"\n  decoding=\"async\"\n  alt=\"Product\">\n```\n\n## Font optimization\n\n### Loading strategy\n```css\n/* System font stack as fallback */\nbody {\n  font-family: 'Custom Font', -apple-system, BlinkMacSystemFont, \n               'Segoe UI', Roboto, sans-serif;\n}\n\n/* Prevent invisible text */\n@font-face {\n  font-family: 'Custom Font';\n  src: url('/fonts/custom.woff2') format('woff2');\n  font-display: swap; /* or optional for non-critical */\n  font-weight: 400;\n  font-style: normal;\n  unicode-range: U+0000-00FF; /* Subset to Latin */\n}\n```\n\n### Preloading critical fonts\n```html\n<link rel=\"preload\" href=\"/fonts/heading.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>\n```\n\n### Variable fonts\n```css\n/* One file instead of multiple weights */\n@font-face {\n  font-family: 'Inter';\n  src: url('/fonts/Inter-Variable.woff2') format('woff2-variations');\n  font-weight: 100 900;\n  font-display: swap;\n}\n```\n\n## Caching strategy\n\n### Cache-Control headers\n```\n# HTML (short or no cache)\nCache-Control: no-cache, must-revalidate\n\n# Static assets with hash (immutable)\nCache-Control: public, max-age=31536000, immutable\n\n# Static assets without hash\nCache-Control: public, max-age=86400, stale-while-revalidate=604800\n\n# API responses\nCache-Control: private, max-age=0, must-revalidate\n```\n\n### Service worker caching\n```javascript\n// Cache-first for static assets\nself.addEventListener('fetch', (event) => {\n  if (event.request.destination === 'image' ||\n      event.request.destination === 'style' ||\n      event.request.destination === 'script') {\n    event.respondWith(\n      caches.match(event.request).then((cached) => {\n        return cached || fetch(event.request).then((response) => {\n          const clone = response.clone();\n          caches.open('static-v1').then((cache) => cache.put(event.request, clone));\n          return response;\n        });\n      })\n    );\n  }\n});\n```\n\n## Runtime performance\n\n### Avoid layout thrashing\n```javascript\n// ❌ Forces multiple reflows\nelements.forEach(el => {\n  const height = el.offsetHeight; // Read\n  el.style.height = height + 10 + 'px'; // Write\n});\n\n// ✅ Batch reads, then batch writes\nconst heights = elements.map(el => el.offsetHeight); // All reads\nelements.forEach((el, i) => {\n  el.style.height = heights[i] + 10 + 'px'; // All writes\n});\n```\n\n### Debounce expensive operations\n```javascript\nfunction debounce(fn, delay) {\n  let timeout;\n  return (...args) => {\n    clearTimeout(timeout);\n    timeout = setTimeout(() => fn(...args), delay);\n  };\n}\n\n// Debounce scroll/resize handlers\nwindow.addEventListener('scroll', debounce(handleScroll, 100));\n```\n\n### Use requestAnimationFrame\n```javascript\n// ❌ May cause jank\nsetInterval(animate, 16);\n\n// ✅ Synced with display refresh\nfunction animate() {\n  // Animation logic\n  requestAnimationFrame(animate);\n}\nrequestAnimationFrame(animate);\n```\n\n### Virtualize long lists\n```javascript\n// For lists > 100 items, render only visible items\n// Use libraries like react-window, vue-virtual-scroller, or native CSS:\n.virtual-list {\n  content-visibility: auto;\n  contain-intrinsic-size: 0 50px; /* Estimated item height */\n}\n```\n\n### Smooth navigations with View Transitions\n\nThe [View Transitions API](https://developer.chrome.com/docs/web-platform/view-transitions) lets the browser cross-fade (or custom-animate) between two DOM states using a single GPU-composited snapshot — no double-render, no layout thrash, and the snapshot doesn't count toward CLS.\n\n**Same-document (SPA-style) — Baseline 2026:**\n```javascript\n// Wrap the DOM mutation that swaps the view\nfunction navigate(newView) {\n  if (!document.startViewTransition) return swapDOM(newView);\n  document.startViewTransition(() => swapDOM(newView));\n}\n```\n\n**Cross-document (MPA-style) — Chromium-stable, progressive enhancement elsewhere:**\n```css\n/* On both source and destination pages */\n@view-transition { navigation: auto; }\n```\nThat's the entire integration — same-origin navigations now fade automatically. To opt specific elements into shared-element transitions (e.g. a thumbnail expanding into a hero), give them a matching `view-transition-name`:\n```css\n.product-thumb[data-id=\"42\"], .product-hero { view-transition-name: product-42; }\n```\n\nPair this with Speculation Rules (above) for instant + animated navigations.\n\n## Third-party scripts\n\n### Load strategies\n```javascript\n// ❌ Blocks main thread\n<script src=\"https://analytics.example.com/script.js\"></script>\n\n// ✅ Async loading\n<script async src=\"https://analytics.example.com/script.js\"></script>\n\n// ✅ Delay until interaction\n<script>\ndocument.addEventListener('DOMContentLoaded', () => {\n  const observer = new IntersectionObserver((entries) => {\n    if (entries[0].isIntersecting) {\n      const script = document.createElement('script');\n      script.src = 'https://widget.example.com/embed.js';\n      document.body.appendChild(script);\n      observer.disconnect();\n    }\n  });\n  observer.observe(document.querySelector('#widget-container'));\n});\n</script>\n```\n\n### Facade pattern\n```html\n<!-- Show static placeholder until interaction -->\n<div class=\"youtube-facade\" \n     data-video-id=\"abc123\" \n     onclick=\"loadYouTube(this)\">\n  <img src=\"/thumbnails/abc123.jpg\" alt=\"Video title\">\n  <button aria-label=\"Play video\">▶</button>\n</div>\n```\n\n## Measurement\n\n### Key metrics\n| Metric | Target | Tool |\n|--------|--------|------|\n| LCP | < 2.5s | Lighthouse, CrUX |\n| FCP | < 1.8s | Lighthouse |\n| Speed Index | < 3.4s | Lighthouse |\n| TBT | < 200ms | Lighthouse |\n| TTI | < 3.8s | Lighthouse |\n\n### Testing commands\n```bash\n# Lighthouse CLI\nnpx lighthouse https://example.com --output html --output-path report.html\n\n# Web Vitals library\nimport {onLCP, onINP, onCLS} from 'web-vitals';\nonLCP(console.log);\nonINP(console.log);\nonCLS(console.log);\n```\n\n## References\n\nFor Core Web Vitals specific optimizations, see [Core Web Vitals](../core-web-vitals/SKILL.md).","author":"@addyosmani","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/addyosmani/web-quality-skills/tree/main/skills/performance","license":"MIT","category":"review","lang":"en","tokens":2872,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":["analytics.example.com","blog.cloudflare.com","cdn.example.com","developer.chrome.com","widget.example.com"]}}