Long Animation Frames API: 프로덕션 환경에서의 INP 디버깅

인터랙션을 망치는 스크립트를 찾기 위한 프로덕션 플레이북

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2026-04-30

수년 동안 "내 INP 문제의 원인이 무엇인가"라는 질문은 종종 공허한 시선으로 돌아왔습니다. Long Tasks API는 메인 스레드가 50ms 이상 차단되었다는 사실만 알려주었습니다. 어떤 스크립트인지, 어떤 줄인지 알려주지 않았습니다. 우리가 가진 것은 밀리초(ms) 단위의 숫자뿐이었습니다.

Long Animation Frames API는 이 문제를 해결합니다. Chrome 123 이후의 Chromium 브라우저에 탑재되었으며 프레임의 전체 해부도를 제공합니다: 전체 지속 시간, 차단 지속 시간, 렌더러가 시작된 순간, 스타일 및 레이아웃이 시작된 순간, 프레임 내에서 5ms 이상 실행된 모든 스크립트 배열을 보여줍니다. 각 스크립트 항목에는 소스 URL, 함수 이름, 파일 내의 문자 위치, 그리고 이를 호출한 콜백의 유형이 포함됩니다.

이것이 바로 INP 디버깅에 항상 필요했던 데이터입니다. 이 글의 나머지 부분에서는 상황을 악화시키지 않고 실제 프로덕션 사이트에서 이를 활용하는 방법에 대해 설명합니다.

이 페이지는 우리의 Interaction to Next Paint (INP) 시리즈의 일부입니다. 아래의 LoAF 플레이북은 제가 지표를 진단한 후 실행하는 방법입니다. INP가 처음이라면 input delay, processing time, presentation delay의 3단계 가이드와 더 광범위한 find-and-fix INP 워크플로를 먼저 시작하십시오.

Long Tasks만으로는 부족했던 이유

long task는 메인 스레드에서 단일 작업이 50ms 이상 실행될 때 발생합니다. 이는 명백한 차단 요소를 잡는 데 유용합니다. 하지만 대부분의 느린 인터랙션은 단일 long task가 아니기 때문에 INP에는 쓸모가 없습니다. 그것들은 중간 규모의 작업 시퀀스와 긴 렌더링의 조합입니다.

인터랙션은 long task를 한 번도 트리거하지 않고도 INP에 실패할 수 있습니다. 40ms의 이벤트 핸들러 다음에 70ms의 스타일 재계산이 이어지면 총 110ms의 프레젠테이션 지연이 발생합니다. Long Tasks API는 아무것도 보고하지 않습니다. 하지만 사용자는 지연을 느낍니다.

Long Tasks의 또 다른 실패는 어트리뷰션(귀인)입니다. 컨테이너 유형, 컨테이너 이름, 그리고 long task가 "self", "same-origin-ancestor", "same-origin-descendant", "unknown" 또는 몇 가지 교차 출처 변형 중 무엇인지 알려주는 이름 필드만 얻게 됩니다. 어떤 스크립트가 실행되었는지는 알 수 없습니다. 따라서 long task를 발견하더라도 DevTools를 열고 인터랙션을 다시 기록하여 원인을 찾아야 합니다. 이는 랩(실험실) 환경에서는 작동합니다. 하지만 재현할 수 없는 실제 사용자의 INP에는 작동하지 않습니다.

LoAF는 이 두 가지 한계를 모두 극복합니다. 단일 작업이 아닌 전체 프레임을 캡처합니다. 그리고 URL과 함수 이름을 통해 어떤 스크립트가 실행되었는지 정확히 알려줍니다.

중요한 7가지 필드

LoAF 항목에는 필요 이상의 필드가 있습니다. 다음 7가지는 제가 인터랙션을 디버깅할 때 가장 먼저 읽는 필드입니다.

duration은 프레임의 전체 길이를 밀리초 단위로 나타냅니다. LoAF 항목은 이 값이 50ms를 초과할 때만 생성됩니다. 이 숫자만으로도 프레임에 문제가 있는지 알 수 있지만, 이유는 알 수 없습니다.

blockingDuration이 더 유용합니다. 이는 long task 임계값을 초과하는 프레임의 각 부분에서 50ms를 뺀 값을 합산합니다. duration: 320blockingDuration: 0인 프레임은 대부분 렌더링 작업입니다. duration: 320blockingDuration: 270인 프레임은 긴 스크립트입니다. 전자는 렌더링 수정이 필요하고, 후자는 코드 변경이 필요합니다.

renderStart는 브라우저가 해당 프레임에 대한 렌더링 작업을 시작한 타임스탬프입니다. startTimerenderStart 사이의 모든 것은 스크립트 실행입니다. 그 이후의 모든 것은 스타일, 레이아웃 및 페인트입니다. 이는 병목 현상이 JavaScript인지 렌더링인지 알려주기 때문에 INP 디버깅에서 가장 유용한 단일 경계입니다.

styleAndLayoutStart는 렌더링이 애니메이션 프레임 콜백 실행에서 실제 스타일 재계산 및 레이아웃으로 이동한 타임스탬프입니다. renderStartstyleAndLayoutStart 사이의 간격은 requestAnimationFrame 콜백입니다. styleAndLayoutStart와 프레임의 끝 사이의 간격은 브라우저 측의 스타일 및 레이아웃 작업입니다.

firstUIEventTimestamp는 프레임 중에 사용자 입력이 도착한 시점입니다. 이 값이 0이 아니면 프레임에 인터랙션이 포함된 것입니다. 이것이 web-vitals.js가 LoAF 항목을 INP 측정값과 상관시키는 방식입니다. firstUIEventTimestamp가 없다는 것은 프레임이 느리지만 기다리고 있는 사용자가 없었음을 의미합니다.

scripts는 어트리뷰션을 위해 읽는 배열입니다. 각 항목은 최소 5ms 동안 실행된 스크립트입니다. sourceURL, sourceFunctionName, invoker, invokerType, durationforcedStyleAndLayoutDuration을 포함합니다. 마지막 필드가 매우 중요합니다. 스크립트가 동기식 레이아웃을 트리거했는지 알려주는데, 이는 제가 감사에서 발견하는 대부분의 처리 시간 병목 현상의 근본 원인입니다.

각 스크립트의 invokerType 필드는 호출 방식을 알려줍니다. 전체 목록은 event-listener (클릭, 스크롤, 키다운), user-callback (setTimeout, setInterval, requestAnimationFrame), resolve-promisereject-promise (then/catch 핸들러), classic-script, 그리고 module-script입니다. INP의 경우 가장 먼저 확인해야 할 것은 처리 시간을 위한 event-listener와 input delay를 위한 user-callback, classic-script 또는 module-script입니다. Promise 핸들러가 목록의 최상단에 있는 경우는 드뭅니다.

이러한 필드들을 세 가지 INP 단계에 매핑하면 그림이 명확해집니다. Input delay는 firstUIEventTimestamp 전에 실행되는 스크립트로 나타납니다. 처리 시간은 UI 이벤트 이후에 실행되는 event-listener 스크립트입니다. 프레젠테이션 지연은 renderStart부터 프레임의 끝까지 모든 것입니다.

web-vitals.js로 프로덕션에서 LoAF 캡처하기

LoAF 옵저버를 처음부터 구축할 필요는 없습니다. web-vitals.js 어트리뷰션 빌드가 이를 대신 수행하며 LoAF 항목을 실제 INP 측정값과 연결해 줍니다. 이것이 제가 고객사 사이트에 설치하는 버전입니다.

import { onINP } from 'web-vitals/attribution';

onINP((metric) => {
    const a = metric.attribution;

    const payload = {
        value: metric.value,
        rating: metric.rating,
        url: location.pathname,
        loadState: a.loadState,
        interactionTarget: a.interactionTarget,
        interactionType: a.interactionType,
        inputDelay: a.inputDelay,
        processingDuration: a.processingDuration,
        presentationDelay: a.presentationDelay,
        longestScriptDuration: a.longestScript?.entry?.duration,
        longestScriptInvoker: a.longestScript?.entry?.invoker,
        longestScriptSource: a.longestScript?.entry?.sourceURL,
        longestScriptSubpart: a.longestScript?.subpart,
        longestScriptIntersecting: a.longestScript?.intersectingDuration,
    };

    navigator.sendBeacon('/rum', JSON.stringify(payload));
});

해당 payload는 샘플링 없이 보낼 수 있을 만큼 작습니다. 진단 작업의 대부분은 longestScriptInvoker, longestScriptSource, longestScriptSubpart 세 가지 필드가 수행합니다. 처음 두 개는 무엇이 실행되었는지 알려줍니다. 세 번째는 그것이 어느 INP 단계에 속하는지 알려줍니다.

가장 긴 스크립트뿐만 아니라 전체 어트리뷰션을 원한다면 a.longAnimationFrameEntries도 함께 보내십시오. 하나의 INP 측정값에는 여러 LoAF가 포함될 수 있으며 무거운 LoAF에는 10개 이상의 스크립트 항목이 포함될 수 있다는 점에 유의하세요. 모든 페이지 뷰에서 전체 배열을 전송하는 것은 비용이 많이 듭니다. 저는 주로 metric.rating !== 'good'일 때만 전체 배열을 비콘(beacon)으로 전송합니다.

한 가지 중요한 세부 사항이 있습니다. LoAF와 INP 후보 인터랙션이 겹치지 않거나 브라우저가 LoAF를 전혀 지원하지 않는 경우 longAnimationFrameEntries 배열은 비어 있게 됩니다. 빈 배열을 정상 신호로 취급하기 전에 항상 기능 감지를 먼저 수행하십시오.

const loafSupported = PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame');

개인정보 보호 고려 사항입니다. 스크립트 sourceURL 값에는 쿼리 매개변수와 전체 경로가 포함될 수 있습니다. 고객사 작업을 위해 저는 RUM으로 전송하기 전에 오리진과 경로명을 제외한 모든 것을 제거합니다. 해시된 번들 파일 이름은 배포 내에서 디버깅하는 데는 유용하지만 여러 배포에 걸쳐 그룹화하는 데는 쓸모가 없으므로 해시 세그먼트도 제거합니다.

925,000개 URL의 어트리뷰션 패턴

CoreDash는 우리가 모니터링하는 사이트의 모든 INP 측정에 대한 LoAF 어트리뷰션을 캡처합니다. 925,000개의 URL 데이터셋 전반에서, 몇 가지 동일한 스크립트 카테고리가 반복해서 차단 LoAF의 주된 원인으로 나타납니다.

CoreDash dashboard showing LoAF attribution grouped by script origin

전자상거래 사이트에서 상위 차단 원인은 일관적입니다. 동기식 맞춤 HTML 태그를 실행하는 태그 관리자가 첫 번째입니다. 지연 바인딩 스크립트 삽입을 수행하는 동의 관리 플랫폼이 두 번째입니다. 광고 및 입찰 스크립트 오케스트레이션이 세 번째입니다. 네 번째는 대규모 클라이언트 측 하이드레이션을 수행하는 상품 추천 위젯입니다. 다섯 번째는 모든 인터랙션을 계측하는 세션 리플레이 스크립트입니다.

고객을 놀라게 하는 것은 forcedStyleAndLayoutDuration의 비중입니다. 전형적으로 불량한 INP 프레임에서 가장 긴 스크립트 지속 시간의 30~60%는 동기식 레이아웃을 트리거하는 스크립트입니다. 스크립트가 느린 이유는 JavaScript가 무거워서가 아닙니다. DOM에 쓰기를 수행하는 루프 내에서 JavaScript가 레이아웃(offsetHeight, getBoundingClientRect)을 읽기 때문에 느려지는 것입니다. 즉, 레이아웃 스래싱(Layout thrashing)입니다. 15년 동안 개발자들이 만들어 온 동일한 문제가 제가 감사하는 사이트에서 여전히 처리 시간을 차지하는 가장 큰 단일 원인입니다.

우리의 데이터셋 전반에서, INP와 교차하는 가장 긴 스크립트의 절반가량을 event-listener 호출자(invoker)가 차지합니다. user-callback (setTimeout, setInterval, requestAnimationFrame)은 약 4분의 1을 차지합니다. 나머지의 대부분은 classic-script 최상위 수준 실행이 차지합니다. Promise 리졸버가 가장 긴 스크립트인 경우는 거의 없으며, 이는 "비동기 코드가 원인이다"라는 INP에 대한 일반적인 가정과 모순됩니다.

CoreDash chart showing invokerType distribution for INP-blocking LoAF scripts

가장 자주 보는 5가지 LoAF 패턴

서드파티 이벤트 핸들러

가장 긴 스크립트의 invokerType: "event-listener"이고 sourceURL이 서드파티 오리진에서 옵니다. 스크립트 지속 시간이 프레임의 대부분을 차지하며 forcedStyleAndLayoutDuration은 낮습니다. 이것은 클릭을 리슨하고 핸들러에서 너무 많은 작업을 수행하는 벤더 태그입니다. "태그를 제거하라"가 해결책이 되는 경우는 드뭅니다. 해결책은 태그가 수행하는 작업을 지연시키는 것입니다. 특히 dataLayer 푸시의 경우 분석을 중단하지 않고 20~100ms를 되돌릴 수 있는 패턴이 있으며, 이는 Scheduling dataLayer Events to optimize the INP에서 다루었습니다.

자체 핸들러 내부의 레이아웃 스래싱

가장 긴 스크립트의 invokerType: "event-listener"이고, sourceURL은 자체 번들이며, forcedStyleAndLayoutDuration이 스크립트 지속 시간의 30% 이상입니다. 스크립트가 루프 내에서 레이아웃을 읽고 있습니다. 해결책은 쓰기 전에 읽기를 일괄 처리하거나, 작업을 requestAnimationFrame으로 이동하고 캐시된 값을 사용하는 것입니다.

저는 전자상거래 사이트의 상품 필터 페이지에서 이를 가장 자주 봅니다. 필터 핸들러는 눈에 보이는 상품 수를 업데이트한 다음, 스티키(sticky) 필터 버튼의 위치를 조정하기 위해 결과 목록의 새로운 높이를 읽고, 그 높이를 기반으로 CSS 사용자 지정 속성을 업데이트한 다음, 높이를 다시 읽습니다. 한 핸들러에서 네 번의 강제 레이아웃이 일어납니다. LoAF는 그 80~150ms의 forcedStyleAndLayoutDuration을 단일 스크립트 항목에 표시하여 수정 방향을 명확하게 합니다. 한 번 읽고, 한 번 쓰고, 종료하십시오.

첫 인터랙션 시의 하이드레이션 캐스케이드

loadState가 여전히 "loading" 또는 "dom-interactive"인 상태에서 LoAF가 발생합니다. firstUIEventTimestampscripts에 프레임워크 하이드레이션 콜백이 포함된 프레임 내에 위치합니다. 사용자가 페이지 하이드레이션 전에 클릭한 것입니다. 해결책은 하이드레이션을 더 빠르게 만드는 것이 아닙니다(물론 도움은 되지만요). 진정한 해결책은 네이티브 링크 및 폼 제출, 네이티브 디스클로저 위젯, 네이티브 대화상자와 같이 인터랙티브 요소가 하이드레이션 없이도 작동하도록 만드는 것입니다. 바로 점진적 향상(Progressive enhancement)입니다.

무거운 DOM으로 인한 프레젠테이션 지연

이 패턴은 데이터에서 다르게 나타납니다. blockingDuration은 낮습니다. scripts 배열은 짧습니다. 하지만 styleAndLayoutStart와 프레임 끝 사이의 간격이 100ms 이상입니다. 어떤 JavaScript 수정도 도움이 되지 않습니다. 렌더러가 수천 개의 노드에 걸쳐 스타일을 재계산하느라 멈춰 있는 것입니다. 화면 밖 섹션에는 content-visibility: auto를 사용하고, 무거운 컴포넌트에는 contain: layout style을 사용하며, 인터랙션당 다시 계산해야 하는 스타일 범위를 줄이십시오.

rAF 폭풍

여러 LoAF가 체인으로 연결되며 각각은 Window.requestAnimationFrame에 의해 호출된 user-callback 스크립트를 갖습니다. 페이지가 사용자와 경쟁하는 JavaScript 애니메이션을 실행하고 있습니다. JavaScript로 구동되는 스크롤 동작이 가장 흔한 버전입니다. 아무도 적용한 것을 기억하지 못하는 스무스 스크롤(smooth-scroll) 폴리필 하나 때문에, 사이트의 모든 페이지에 있는 모든 클릭에 200ms의 프레젠테이션 지연이 더해지는 것을 본 적이 있습니다. 스크롤 케이스에 대해서는 Improve the INP by ditching JavaScript scrolling에서 다루었습니다. 모든 JavaScript 애니메이션에 대해 동일한 논리가 적용됩니다. CSS 또는 Web Animations API로 전환하면 LoAF가 사라집니다.

현장에서의 LoAF payload 모습

결제 페이지 감사에서 나온 실제 LoAF 항목입니다. URL은 익명화했지만 구조와 타이밍은 그대로입니다.

{
    "name": "long-animation-frame",
    "entryType": "long-animation-frame",
    "startTime": 5902,
    "duration": 320,
    "blockingDuration": 268,
    "renderStart": 6170,
    "styleAndLayoutStart": 6172,
    "firstUIEventTimestamp": 5913,
    "scripts": [
        {
            "name": "script",
            "entryType": "script",
            "invoker": "BUTTON#checkout-submit.onclick",
            "invokerType": "event-listener",
            "sourceURL": "https://cdn.example.com/app.HASH.js",
            "sourceFunctionName": "handleCheckoutSubmit",
            "duration": 248,
            "forcedStyleAndLayoutDuration": 142,
            "executionStart": 5914,
            "startTime": 5914
        }
    ]
}

이렇게 해석하십시오. 페이지 로드 후 5902ms에 프레임이 시작되었습니다. 5913ms에 사용자가 결제 제출 버튼을 클릭했습니다. 클릭 핸들러는 5914ms에 시작되어 248ms 동안 실행되었습니다. 그 248ms 중 142ms는 강제 동기식 레이아웃이었습니다. 렌더러는 클릭 후 0.25초가 넘는 6170ms가 되어서야 시작되었습니다. 이 인터랙션의 INP는 약 270ms로 "개선 필요(needs improvement)" 영역에 있습니다.

해결책은 "handleCheckoutSubmit을 더 빠르게 만드는 것"이 아닙니다. 해결책은 "handleCheckoutSubmit 내부에서 강제 레이아웃을 중단하는 것"입니다. 248ms 중 142ms는 JavaScript가 아닙니다. 그것은 JavaScript 쓰기로 인해 무효화된 레이아웃 값을 읽는 작업입니다. 이 데이터가 나온 실제 사례에서, 쓰기 루프 전에 읽은 값을 한 번 캐싱했더니 스크립트 지속 시간이 248ms에서 110ms로 떨어졌습니다. 결제 페이지의 INP는 p75 270ms에서 200ms 미만으로 이동했습니다. 그 페이지는 기준을 통과했습니다.

이것은 어떤 랩 도구에도 나타나지 않는 종류의 수정입니다. Lighthouse는 페이지와 상호 작용하지 않기 때문에 forcedStyleAndLayoutDuration에 대해 알려줄 수 없습니다. 현장에서의 LoAF가 이를 볼 수 있는 유일한 방법입니다.

DevTools Console output showing the LoAF summary table printed by the webperf-snippets script

LoAF 데이터로 AI 에이전트 구동하기

AI 지원 디버깅에서 LoAF가 중요한 이유는 에이전트에게 논리적으로 판단할 수 있는 구체적인 데이터를 제공하기 때문입니다. 필드 데이터가 없는 에이전트는 코드에서 INP 원인을 추측할 수밖에 없습니다. 실제 사용자 세션의 LoAF 항목을 가진 에이전트는 스크립트, 함수 및 단계를 정확히 지정할 수 있습니다. 진단이 더 이상 추측에 머물지 않습니다.

제가 Claude Code와 함께 사용하는 워크플로는 다음과 같습니다: MCP 서버를 통해 CoreDash에서 최악의 INP 페이지를 가져오고, 해당 URL의 가장 느린 인터랙션에 대한 LoAF 항목을 첨부한 다음, 각 항목이 위의 5가지 패턴 중 어느 것에 해당하는지 식별하도록 에이전트에게 요청합니다. 에이전트는 각 LoAF를 분류하고 코드베이스의 함수를 가리키며 수정안을 제안합니다. 저는 수정을 검토합니다. 저는 그것을 적용합니다. 그리고 CoreDash는 p75 INP가 움직이는지 측정합니다.

이 방식이 잘 작동하는 이유는 LoAF 데이터가 구조화되어 있고 작기 때문입니다. 단일 LoAF JSON payload는 에이전트 컨텍스트 창의 아주 작은 부분에 들어갑니다. 단일 페이지에 대한 소수의 대표적인 LoAF만으로도 수정을 유도하기에 충분합니다. 이보다 더 광범위한 패턴에 대해서는 Fix INP with an AI agent: the metric lab tools cannot measureAI Agent Core Web Vitals: why field data changes everything에 글을 작성했습니다.

크로스 브라우저 현실

LoAF는 Chromium 전용입니다. Safari는 이를 구현하지 않습니다. Firefox도 구현하지 않습니다. 이제 Safari 26.2와 Firefox 144에서 INP 자체가 제공되므로 측정에 간극이 생깁니다. INP RUM 데이터는 크로스 브라우저입니다. 하지만 어트리뷰션 데이터는 Chrome과 Edge에만 국한됩니다.

저는 이 때문에 고객들이 LoAF 채택을 주저하는 것을 봅니다. 그들은 Safari가 이를 배포할 때까지 기다리고 싶어 합니다. 그것은 잘못된 판단입니다. Chrome은 대부분의 성능 버그가 먼저 재현되고 대부분의 성능 수정이 처음 검증되는 곳입니다. 문제의 형태(긴 이벤트 핸들러, 레이아웃 스래싱, 하이드레이션 캐스케이드)는 브라우저 간에 변하지 않습니다. 진단 렌즈만 다를 뿐입니다. Chromium LoAF 데이터를 기반으로 배포한 수정 사항은 Safari 및 Firefox 사용자의 INP도 개선합니다.

오늘날 Safari에서 어떤 형태의 어트리뷰션이라도 원한다면 EventTiming을 사용할 수 있습니다. 어떤 이벤트가 느렸는지는 알려주지만 어떤 스크립트인지는 알려주지 않습니다. 어디를 봐야 할지 아는 데는 충분하지만 무엇을 수정해야 할지는 알 수 없습니다. Safari 어트리뷰션을 위해서는 Safari Web Inspector의 랩 프로파일링이 실질적인 fallback입니다.

LoAF가 여전히 알려줄 수 없는 것

어느 시점에는 맞닥뜨리게 될 3가지 맹점에 대해 알아둘 필요가 있습니다.

첫 번째는 교차 출처(cross-origin) iframe입니다. LoAF는 문서의 출처를 공유하는 창의 메인 스레드에서 실행되는 스크립트에만 어트리뷰션을 제공합니다. 서드파티 iframe 내부의 차단 스크립트는 긴 프레임과 빈 scripts 배열이라는 불투명한 어트리뷰션으로 나타납니다. 수정 방법은 다른 서드파티 iframe 성능 문제와 같습니다: iframe 로드를 지연하고, 명시적인 치수를 제공하며, 스크롤을 내려야 보이는 곳(below the fold)에 있다면 loading="lazy"를 사용하십시오.

워커(Workers)가 두 번째 맹점입니다. 웹 워커는 LoAF 항목에 나타나지 않는데, 이는 LoAF가 메인 스레드 API이기 때문입니다. 워커가 메인 스레드로 무거운 메시지를 다시 보내서 INP가 나빠진 경우, LoAF는 메인 스레드의 메시지 핸들러만 보여주고 이를 트리거한 워커의 작업은 보여주지 않습니다. 워커 내부에서 성능 마크(performance marks)를 사용하여 수동으로 교차 참조해야 합니다.

마지막으로 GPU 컴포지터 지연입니다. LoAF 항목의 끝은 렌더러가 메인 스레드에서 작업을 완료하는 시점입니다. 화면에 픽셀이 표시되는 실제 순간은 컴포지터와 GPU에서 그 이후에 발생합니다. 저사양 Android 기기에서는 컴포지터 지연으로 인해 LoAF가 볼 수 없는 30~100ms가 추가될 수 있습니다. LoAF 항목의 presentationTime 필드(현재 실험적인 PaintTimingMixin 플래그 뒤에 있음)는 이를 해결하기 위한 것이지만 아직 Chrome 버전 간에 안정적이지 않습니다.

이 중 어느 것도 치명적이지 않습니다. 단지 LoAF가 모든 이야기를 다 해주는 것은 아님을 의미할 뿐입니다. 실제 고객 사이트에서 발생하는 INP 문제의 80%에 대해서는 이것으로 충분합니다.

어디서부터 시작할 것인가

LoAF 데이터를 본 적이 없다면 랩이 아닌 현장에서 시작하십시오. web-vitals.js 어트리뷰션 빌드를 설치하고, INP 측정당 가장 긴 스크립트를 비콘으로 전송하고, 일주일간의 트래픽에 걸쳐 상위 10개의 longestScriptSource 값을 확인하십시오. 맨 위에 있는 것이 무엇이든 간에, Lighthouse 보고서의 내용과 관계없이 그것이 여러분의 첫 번째 수정 대상입니다.

랩 뷰를 원한다면, Chrome DevTools 성능(Performance) 패널은 기본적으로 LoAF를 표시하지 않지만 PerformanceObserver를 통해 데이터를 사용할 수 있습니다. devtools 세부 정보 필드와 함께 performance.measure를 사용하는 맞춤 트랙 접근 방식을 통해 패널 내부에 LoAF 항목을 표시할 수 있습니다. webperf-snippets 프로젝트에는 인터랙션과 함께 LoAF 항목의 요약 테이블을 인쇄하는 콘솔 스니펫이 있으며, 이것이 제가 특정 페이지를 디버깅할 때 보통 가장 먼저 실행하는 것입니다.

나머지 INP 시리즈 둘러보기

LoAF 플레이북은 더 큰 워크플로 안에 속합니다. 각 단계나 절차에 대해 더 깊이 알아보려면:

  • Find and fix INP issues: RUM 데이터에서 느린 인터랙션을 찾아내는 진단 흐름입니다.
  • Input delay: 이벤트 핸들러가 시작되기 전에 실행되는 스크립트입니다.
  • Processing time: 이벤트 핸들러 자체입니다.
  • Presentation delay: 핸들러가 반환된 후의 렌더링 작업입니다.
  • INP hub: 지표에 대한 전체 가이드입니다.

출처: Chrome for Developers, Long Animation Frames API, MDN, Long animation frame timing, W3C Long Animation Frames specification, GoogleChrome/web-vitals library.

About the author

Arjen Karel is a web performance consultant and the creator of CoreDash, a Real User Monitoring platform that tracks Core Web Vitals data across hundreds of sites. He also built the Core Web Vitals Visualizer Chrome extension. He has helped clients achieve passing Core Web Vitals scores on over 925,000 mobile URLs.

CoreDash는 제가 직접 쓰려고 만들었습니다.

1KB 미만, EU 호스팅, 쿠키 동의 배너 없음. 이제 MCP까지 지원합니다.

CoreDash 무료 체험

Long Animation Frames API 질문과 답변

브라우저 지원 및 LoAF가 대체하는 것

Long Animation Frames API는 모든 브라우저에서 지원되나요?

아닙니다. LoAF는 Chromium 전용입니다. Chrome, Edge, Opera, Brave에서는 지원됩니다. Safari와 Firefox는 지원하지 않습니다. 이 기능에 의존하기 전에 항상 PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame')로 기능 감지를 먼저 수행하십시오.

LoAF가 Long Tasks API를 대체하나요?

INP 디버깅의 경우, 그렇습니다. Long Tasks는 스크립트 어트리뷰션 없이 메인 스레드 차단 지속 시간만 제공합니다. LoAF는 동일한 차단 신호와 함께 실제로 실행된 스크립트 정보를 제공합니다. 새로운 INP 작업에 Long Tasks를 사용할 이유는 없습니다. 향후 Total Blocking Time도 LoAF의 blockingDuration 측면에서 재정의될 수 있습니다.

LoAF 데이터 읽기

LoAF 항목을 특정 INP 측정과 어떻게 연결하나요?

web-vitals.js 어트리뷰션 빌드가 attribution.longAnimationFrameEntries를 통해 이 작업을 대신 수행합니다. INP 후보 인터랙션과 타이밍 윈도우가 겹치는 LoAF 항목을 찾습니다. 자체 옵저버를 만드는 경우, entry.startTimeentry.startTime + entry.duration을 인터랙션 시간과 비교하고 윈도우가 교차하는 LoAF를 포함하십시오.

forcedStyleAndLayoutDuration은 실제로 무엇을 의미하나요?

이는 스크립트가 동기식 레이아웃을 트리거하는 데 쓴 시간입니다. JavaScript가 DOM 변경 후 최신 레이아웃이 필요한 offsetHeight, getBoundingClientRect 또는 기타 속성을 읽을 때, 브라우저는 스크립트 내에서 동기식으로 레이아웃 작업을 수행해야 합니다. 그 작업 시간은 스크립트에 어트리뷰트됩니다. 높은 forcedStyleAndLayoutDuration은 거의 항상 레이아웃 스래싱을 의미합니다.

일부 LoAF 항목에서 scripts 배열이 비어 있는 이유는 무엇인가요?

세 가지 이유가 있습니다. 첫째, 해당 항목은 최소 5ms 동안 실행된 스크립트만 포함합니다. 임계값 미만의 짧은 스크립트로 가득 찬 프레임은 어트리뷰션을 표시하지 않습니다. 둘째, 교차 출처 iframe, 웹 워커, 서비스 워커 또는 브라우저 확장 프로그램 내에서 실행되는 스크립트는 LoAF가 동일 출처의 메인 스레드만 보기 때문에 어트리뷰트되지 않습니다. 셋째, 프레임에 스크립트 작업이 전혀 없고 스타일과 레이아웃만 있을 수 있으며, 이는 프레젠테이션 지연 프레임입니다.

INP 그 이상

LoAF가 INP뿐만 아니라 LCP에도 도움이 될 수 있나요?

네, 간접적으로 도움이 됩니다. LCP 요소가 렌더링되기 전에 실행되는 LoAF는 LCP 렌더링을 지연시키는 긴 스크립트입니다. 종료 시간이 LCP 타임스탬프 이전인 LoAF를 필터링하면, 거기서 가장 긴 스크립트가 LCP 렌더링 지연의 가장 큰 원인입니다. 동일한 어트리뷰션 데이터를 다른 지표에 적용하는 것입니다.