--- id: wiki-2026-0508-long-animation-frames-api title: Long Animation Frames API category: 10_Wiki/Topics status: verified canonical_id: self aliases: [LoAF, Long Animation Frames, LongAnimationFrameTiming] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [web-performance, loaf, long-tasks, inp, scripting-attribution] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: { language: js, framework: web-api } --- # Long Animation Frames API ## 매 한 줄 > **"매 Long Tasks 의 후계자"**. 50ms+ frame 을 잡고 어떤 script 가 원인인지 attribution 까지 — INP 디버깅의 핵심. ## 매 핵심 ### 매 vs Long Tasks | | Long Tasks | Long Animation Frames (LoAF) | |---|---|---| | 단위 | 단일 task | rendering frame 전체 (start ~ render) | | Threshold | 50ms+ | 50ms+ frame duration | | Attribution | 거의 없음 | scripting source URL, invoker, function | | Render 정보 | 없음 | renderStart, styleAndLayoutStart, paint | ### 매 timing 분해 - `startTime` → frame 시작 - `renderStart` → render phase 시작 - `styleAndLayoutStart` → style/layout 시작 - `duration` → 총 frame - `blockingDuration` → blocking 시간 - `scripts[]` → 어떤 스크립트가 얼마나 점유 ### 매 응용 1. INP regression 디버깅 2. 3rd-party script 영향 정량화 3. Hydration cost 측정 (Next/Nuxt) 4. RUM 에 attribution 보내기 5. Frame budget violation 알림 ## 💻 패턴 ### Pattern 1: Basic observer ```js const obs = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.duration < 50) continue; console.log({ duration: entry.duration, blocking: entry.blockingDuration, renderStart: entry.renderStart, scripts: entry.scripts.map(s => ({ name: s.name, invoker: s.invoker, source: s.sourceURL, duration: s.duration, })), }); } }); obs.observe({ type: "long-animation-frame", buffered: true }); ``` ### Pattern 2: Send to RUM with worst script ```js new PerformanceObserver((list) => { for (const e of list.getEntries()) { const worst = e.scripts.reduce((a, b) => b.duration > a.duration ? b : a, { duration: 0 }); sendBeacon("/rum/loaf", JSON.stringify({ url: location.href, duration: e.duration, blocking: e.blockingDuration, worstScript: worst.sourceURL, worstFn: worst.invoker, })); } }).observe({ type: "long-animation-frame", buffered: true }); ``` ### Pattern 3: Budget violation alarm ```js const FRAME_BUDGET_MS = 100; new PerformanceObserver((list) => { for (const e of list.getEntries()) { if (e.duration > FRAME_BUDGET_MS) { reportBudgetMiss({ duration: e.duration, scripts: e.scripts.map(s => s.sourceURL), }); } } }).observe({ type: "long-animation-frame", buffered: true }); ``` ### Pattern 4: 3rd-party attribution ```js function attributeScript(s) { const u = new URL(s.sourceURL || location.href); if (u.hostname === location.hostname) return "first-party"; if (/google|facebook|hotjar|segment/.test(u.hostname)) return "analytics"; return u.hostname; } new PerformanceObserver((list) => { const buckets = {}; for (const e of list.getEntries()) for (const s of e.scripts) { const k = attributeScript(s); buckets[k] = (buckets[k] || 0) + s.duration; } console.table(buckets); }).observe({ type: "long-animation-frame", buffered: true }); ``` ### Pattern 5: Hydration measurement ```js let hydrationFrames = []; new PerformanceObserver((list) => { if (performance.now() < 5000) { // 첫 5s = hydration phase 가정 hydrationFrames.push(...list.getEntries()); } }).observe({ type: "long-animation-frame", buffered: true }); window.addEventListener("load", () => { console.log("Hydration LoAFs:", hydrationFrames.length, "total blocking:", hydrationFrames.reduce((s, e) => s + e.blockingDuration, 0)); }); ``` ### Pattern 6: Tie LoAF to INP event ```js let recentLoafs = []; new PerformanceObserver(list => { recentLoafs.push(...list.getEntries()); recentLoafs = recentLoafs.slice(-20); }).observe({ type: "long-animation-frame", buffered: true }); new PerformanceObserver(list => { for (const ev of list.getEntries()) { const overlapping = recentLoafs.filter(l => l.startTime <= ev.startTime + ev.duration && l.startTime + l.duration >= ev.startTime ); console.log("INP", ev.duration, "overlapping LoAFs:", overlapping); } }).observe({ type: "event", durationThreshold: 40 }); ``` ### Pattern 7: Feature detection ```js const supportsLoAF = PerformanceObserver.supportedEntryTypes?.includes("long-animation-frame"); if (supportsLoAF) { /* observe */ } else { /* fallback to long-task */ } ``` ## 매 결정 기준 | 상황 | API | |---|---| | INP debugging | LoAF (attribution 필수) | | 단순 long task 개수 | longtask 도 충분 | | Production RUM | LoAF (worst script만 전송) | | 3rd-party 영향 분석 | LoAF | | 호환성 필요 | LoAF + longtask fallback | **기본값**: LoAF observer + RUM beacon + budget alarm. ## 🔗 Graph - 부모: [[Web_Performance]] - 변형: [[Long Tasks]] (구세대) - 응용: [[INP]], [[Core Web Vitals Optimization (INP, LCP, CLS)|Core_Web_Vitals]] - Adjacent: [[Lighthouse]], [[RUM]] ## 🤖 LLM 활용 **언제**: LoAF dump → "어떤 script 가 가장 비용 비싼가" 분석, 패턴 인식. **언제 X**: 실시간 sampling 결정 (deterministic threshold), 보안 critical attribution (cross-origin source 제한). ## ❌ 안티패턴 - 모든 LoAF 를 서버로 전송 → bandwidth 폭발 (worst만 전송) - Long Task 만 보고 INP 디버깅 → 원인 못 찾음 - `buffered: true` 안 씀 → 초기 frame 놓침 - Feature detection 없이 사용 → 구 브라우저 throw - Cross-origin script `sourceURL` 가려짐 무시 → "(unknown)" 비율 보고 ## 🧪 검증 / 중복 - Verified (W3C LongAnimationFrameTiming spec, web.dev/articles/long-animation-frames). 신뢰도 A. - Chrome 123+ 안정 지원, Safari/Firefox 진행 중 (2026-05 기준). ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — vs LongTasks, attribution + INP correlation patterns |