1. 소개
이 Codelab에서는 web-vitals
라이브러리를 사용하여 다음 페인트에 대한 상호작용 (INP)을 측정하는 방법을 알아봅니다.
기본 요건
- HTML 및 JavaScript 개발에 관한 지식
- 권장: web.dev INP 측정항목 문서를 읽어보세요.
학습할 내용
web-vitals
라이브러리를 페이지에 추가하고 기여 분석 데이터를 사용하는 방법- 기여 분석 데이터를 사용하여 INP 개선을 시작할 위치와 방법을 진단합니다.
필요한 항목
- GitHub에서 코드를 클론하고 npm 명령어를 실행할 수 있는 컴퓨터
- 텍스트 편집기
- 모든 상호작용 측정이 작동하는 최신 버전의 Chrome
2. 설정
코드 가져오기 및 실행
코드는 web-vitals-codelabs
저장소에 있습니다.
- 터미널에서 저장소(
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
)를 클론합니다. - 클론된 디렉터리(
cd web-vitals-codelabs/measuring-inp
)로 이동합니다. - 종속 항목
npm ci
를 설치합니다. - 웹 서버를 시작합니다.
npm run start
- 브라우저에서 http://localhost:8080/을 방문합니다.
페이지 사용해 보기
이 Codelab에서는 Gastropodicon (인기 있는 달팽이 해부학 참조 사이트)을 사용하여 INP의 잠재적 문제를 살펴봅니다.
페이지와 상호작용하여 속도가 느린 상호작용을 파악해 보세요.
3. Chrome DevTools 설정하기
기타 도구 > 개발자 도구 메뉴에서 DevTools를 엽니다. 또는 페이지를 마우스 오른쪽 버튼으로 클릭하고 검사를 선택하거나 단축키를 사용합니다.
이 Codelab에서는 성능 패널과 콘솔을 모두 사용합니다. DevTools 상단의 탭에서 언제든지 두 모드 간에 전환할 수 있습니다.
- INP 문제는 주로 휴대기기에서 발생하므로 모바일 디스플레이 에뮬레이션으로 전환하세요.
- 데스크톱이나 노트북에서 테스트하는 경우 실제 휴대기기보다 성능이 훨씬 더 좋을 수 있습니다. 실적을 더 현실적으로 확인하려면 실적 패널의 오른쪽 상단에 있는 톱니바퀴를 클릭한 다음 CPU 4배 느리게를 선택합니다.
4. web-vitals 설치
web-vitals
는 사용자가 경험하는 Web Vitals 측정항목을 측정하기 위한 JavaScript 라이브러리입니다. 라이브러리를 사용하여 이러한 값을 캡처한 후 나중에 분석할 수 있도록 분석 엔드포인트로 비콘으로 전송하여 느린 상호작용이 발생하는 시점과 위치를 파악할 수 있습니다.
페이지에 라이브러리를 추가하는 방법에는 여러 가지가 있습니다. 자체 사이트에 라이브러리를 설치하는 방법은 종속 항목을 관리하는 방법, 빌드 프로세스, 기타 요인에 따라 다릅니다. 라이브러리 문서에서 모든 옵션을 확인하세요.
이 Codelab에서는 특정 빌드 프로세스를 자세히 살펴보지 않도록 npm에서 설치하고 스크립트를 직접 로드합니다.
사용할 수 있는 web-vitals
버전에는 두 가지가 있습니다.
- 페이지 로드에서 Core Web Vitals의 측정항목 값을 추적하려면 '표준' 빌드를 사용해야 합니다.
- '기여 분석' 빌드는 각 측정항목에 디버그 정보를 추가하여 측정항목이 특정 값으로 표시되는 이유를 진단합니다.
이 Codelab에서는 INP를 측정하기 위해 기여 분석 빌드가 필요합니다.
npm install -D web-vitals
를 실행하여 프로젝트의 devDependencies
에 web-vitals
를 추가합니다.
페이지에 web-vitals
를 추가합니다.
index.html
하단에 저작자 표시 버전의 스크립트를 추가하고 결과를 콘솔에 로깅합니다.
<script type="module">
import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';
onINP(console.log);
</script>
사용해 보기
콘솔을 열어 페이지와 다시 상호작용해 봅니다. 페이지를 클릭해도 아무것도 기록되지 않습니다.
INP는 페이지의 전체 수명 주기 동안 측정되므로 기본적으로 web-vitals
는 사용자가 페이지를 나가거나 닫을 때까지 INP를 보고하지 않습니다. 이는 애널리틱스와 같은 항목을 비콘으로 표시하는 데는 이상적인 동작이지만 대화형으로 디버그하는 데는 적합하지 않습니다.
web-vitals
는 더 상세한 보고를 위한 reportAllChanges
옵션을 제공합니다. 사용 설정하면 모든 상호작용이 보고되는 것이 아니라 이전 상호작용보다 느린 상호작용이 있을 때마다 보고됩니다.
스크립트에 옵션을 추가하고 페이지와 다시 상호작용해 보세요.
<script type="module">
import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';
onINP(console.log, {reportAllChanges: true});
</script>
페이지를 새로고침하면 이제 상호작용이 콘솔에 보고되고 가장 느린 상호작용이 새로 표시될 때마다 업데이트됩니다. 예를 들어 검색창에 입력한 후 입력한 내용을 삭제해 보세요.
5. 기여 분석에는 어떤 내용이 포함되나요?
대부분의 사용자가 페이지와 처음 상호작용하는 쿠키 동의 대화상자부터 살펴보겠습니다.
많은 페이지에는 사용자가 쿠키를 수락할 때 동기식으로 트리거되는 쿠키가 필요한 스크립트가 있어 클릭이 느린 상호작용이 됩니다. 이 경우 다음과 같은 일이 발생합니다.
예를 클릭하여 (데모) 쿠키를 수락하고 이제 DevTools 콘솔에 로깅된 INP 데이터를 살펴봅니다.
이 최상위 정보는 표준 및 기여 분석 웹 Vitals 빌드에서 모두 사용할 수 있습니다.
{
name: 'INP',
value: 344,
rating: 'needs-improvement',
entries: [...],
id: 'v4-1715732159298-8028729544485',
navigationType: 'reload',
attribution: {...},
}
사용자가 클릭한 시점부터 다음 페인트까지의 시간은 344밀리초로 '개선 필요' INP입니다. entries
배열에는 이 상호작용과 연결된 모든 PerformanceEntry
값이 있습니다. 이 경우 클릭 이벤트가 하나만 있습니다.
이 기간 동안 어떤 일이 일어나고 있는지 알아보려면 attribution
속성이 가장 중요합니다. 기여 분석 데이터를 빌드하기 위해 web-vitals
는 클릭 이벤트와 겹치는 긴 애니메이션 프레임 (LoAF)을 찾습니다. 그러면 LoAF는 실행된 스크립트에서 requestAnimationFrame
콜백, 스타일, 레이아웃에 소비된 시간에 이르기까지 해당 프레임에서 소비된 시간에 관한 자세한 데이터를 제공할 수 있습니다.
attribution
속성을 펼쳐 자세한 내용을 확인하세요. 데이터가 훨씬 더 풍부합니다.
attribution: {
interactionTargetElement: Element,
interactionTarget: '#confirm',
interactionType: 'pointer',
inputDelay: 27,
processingDuration: 295.6,
presentationDelay: 21.4,
processedEventEntries: [...],
longAnimationFrameEntries: [...],
}
먼저 상호작용한 항목에 관한 정보가 있습니다.
interactionTargetElement
: 상호작용한 요소에 대한 실시간 참조입니다 (요소가 DOM에서 삭제되지 않은 경우).interactionTarget
: 페이지 내에서 요소를 찾는 선택자입니다.
다음으로 타이밍이 대략적으로 분류됩니다.
inputDelay
: 사용자가 상호작용을 시작한 시점 (예: 마우스를 클릭함)과 해당 상호작용의 이벤트 리스너가 실행되기 시작한 시점 사이의 시간입니다. 이 경우 CPU 제한을 사용 설정했음에도 입력 지연은 약 27밀리초에 불과했습니다.processingDuration
: 이벤트 리스너가 실행을 완료하는 데 걸리는 시간입니다. 페이지에는 단일 이벤트에 대한 리스너가 여러 개 있는 경우가 많습니다 (예:pointerdown
,pointerup
,click
). 모두 동일한 애니메이션 프레임에서 실행되면 이 시간으로 병합됩니다. 이 경우 처리 시간이 295.6밀리초 소요되며, 이는 INP 시간의 대부분을 차지합니다.presentationDelay
: 이벤트 리스너가 완료된 시점부터 브라우저가 다음 프레임 페인팅을 완료할 때까지의 시간입니다. 이 경우 21.4밀리초입니다.
이러한 INP 단계는 최적화해야 할 항목을 진단하는 데 중요한 신호가 될 수 있습니다. INP 최적화 가이드에서 이 주제에 관한 자세한 내용을 확인하세요.
좀 더 자세히 살펴보면 processedEventEntries
에는 최상위 INP entries
배열의 단일 이벤트와 달리 5개의 이벤트가 포함되어 있습니다. 어떤 차이가 있나요?
processedEventEntries: [
{
name: 'mouseover',
entryType: 'event',
startTime: 1801.6,
duration: 344,
processingStart: 1825.3,
processingEnd: 1825.3,
cancelable: true
},
{
name: 'mousedown',
entryType: 'event',
startTime: 1801.6,
duration: 344,
processingStart: 1825.3,
processingEnd: 1825.3,
cancelable: true
},
{name: 'mousedown', ...},
{name: 'mouseup', ...},
{name: 'click', ...},
],
최상위 항목은 INP 이벤트(이 경우 클릭)입니다. 기여 분석 processedEventEntries
은 동일한 프레임 중에 처리된 모든 이벤트입니다. 클릭 이벤트뿐만 아니라 mouseover
및 mousedown
와 같은 다른 이벤트도 포함되어 있습니다. 이러한 다른 이벤트도 느린 경우 이러한 이벤트에 관해 아는 것이 중요할 수 있습니다. 이러한 이벤트가 모두 응답 속도를 느리게 했기 때문입니다.
마지막으로 longAnimationFrameEntries
배열이 있습니다. 단일 항목일 수도 있지만 상호작용이 여러 프레임에 걸쳐 확장될 수 있는 경우도 있습니다. 다음은 긴 애니메이션 프레임이 하나 있는 가장 간단한 사례입니다.
longAnimationFrameEntries
LoAF 항목 펼치기:
longAnimationFrameEntries: [{
name: 'long-animation-frame',
startTime: 1823,
duration: 319,
renderStart: 2139.5,
styleAndLayoutStart: 2139.7,
firstUIEventTimestamp: 1801.6,
blockingDuration: 268,
scripts: [{...}]
}],
스타일 지정하는 데 걸린 시간을 분석하는 등 유용한 값이 많이 있습니다. Long Animation Frames API 도움말에서 이러한 속성에 대해 자세히 알아보세요. 현재 주로 관심이 있는 것은 scripts
속성으로, 여기에는 장기 실행 프레임의 책임이 있는 스크립트에 관한 세부정보를 제공하는 항목이 포함되어 있습니다.
scripts: [{
name: 'script',
invoker: 'BUTTON#confirm.onclick',
invokerType: 'event-listener',
startTime: 1828.6,
executionStart: 1828.6,
duration: 294,
sourceURL: 'http://localhost:8080/third-party/cmp.js',
sourceFunctionName: '',
sourceCharPosition: 1144
}]
이 경우 BUTTON#confirm.onclick
에서 호출된 단일 event-listener
에서 주로 시간이 소비되었음을 알 수 있습니다. 스크립트 소스 URL과 함수가 정의된 위치의 문자 위치도 확인할 수 있습니다.
요약
이 기여 분석 데이터에서 이 케이스에 관해 무엇을 알 수 있나요?
button#confirm
요소 (attribution.interactionTarget
및 스크립트 기여 분석 항목의invoker
속성)를 클릭하여 상호작용이 트리거되었습니다.- 주로 이벤트 리스너를 실행하는 데 시간이 소요되었습니다 (총 측정항목
value
대비attribution.processingDuration
). - 느린 이벤트 리스너 코드는
third-party/cmp.js
(scripts.sourceURL
)에 정의된 클릭 리스너에서 시작됩니다.
이 정도면 최적화해야 할 부분을 파악하기에 충분한 데이터입니다.
6. 여러 이벤트 리스너
페이지를 새로고침하여 DevTools 콘솔이 깨끗해지고 쿠키 동의 상호작용이 더 이상 가장 긴 상호작용이 아니도록 합니다.
검색창에 입력을 시작합니다. 기여 분석 데이터는 무엇을 보여주나요? 어떻게 생각하시나요?
기여 분석 데이터
먼저 데모 테스트의 한 가지 예를 대략적으로 살펴봅니다.
{
name: 'INP',
value: 1072,
rating: 'poor',
attribution: {
interactionTargetElement: Element,
interactionTarget: '#search-terms',
interactionType: 'keyboard',
inputDelay: 3.3,
processingDuration: 1060.6,
presentationDelay: 8.1,
processedEventEntries: [...],
longAnimationFrameEntries: [...],
}
}
input#search-terms
요소와의 키보드 상호작용에서 CPU 제한이 사용 설정된 상태의 나쁜 INP 값입니다. 총 INP 1,072밀리초 중 1,061밀리초가 처리 시간에 소요되었습니다.
하지만 scripts
항목이 더 흥미롭습니다.
레이아웃 스래싱
scripts
배열의 첫 번째 항목은 다음과 같은 유용한 컨텍스트를 제공합니다.
scripts: [{
name: 'script',
invoker: 'BUTTON#confirm.onclick',
invokerType: 'event-listener',
startTime: 4875.6,
executionStart: 4875.6,
duration: 497,
forcedStyleAndLayoutDuration: 388,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: 'handleSearch',
sourceCharPosition: 940
},
...]
처리 시간의 대부분은 input
리스너인 이 스크립트 실행 중에 발생합니다 (호출자는 INPUT#search-terms.oninput
). 함수 이름은 index.js
소스 파일 내의 문자 위치와 같이 지정됩니다 (handleSearch
).
하지만 새로운 속성 forcedStyleAndLayoutDuration
이 있습니다. 이 스크립트 호출 내에서 브라우저가 페이지 레이아웃을 다시 설정해야 하는 데 걸린 시간입니다. 즉, 이 이벤트 리스너를 실행하는 데 소비된 시간의 78%(497밀리초 중 388밀리초)가 실제로 레이아웃 트래싱에 소비되었습니다.
이 문제를 가장 먼저 해결해야 합니다.
반복된 리스너
다음 두 스크립트 항목은 개별적으로 특별히 주목할 만한 점이 없습니다.
scripts: [...,
{
name: 'script',
invoker: '#document.onkeyup',
invokerType: 'event-listener',
startTime: 5375.3,
executionStart: 5375.3,
duration: 124,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: '',
sourceCharPosition: 1526,
},
{
name: 'script',
invoker: '#document.onkeyup',
invokerType: 'event-listener',
startTime: 5673.9,
executionStart: 5673.9,
duration: 95,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: '',
sourceCharPosition: 1526
}]
두 항목 모두 keyup
리스너이며, 하나가 바로 뒤에 하나씩 실행됩니다. 리스너는 익명의 함수이므로 (따라서 sourceFunctionName
속성에 아무것도 보고되지 않음) 소스 파일과 문자 위치가 남아 있으므로 코드가 있는 위치를 찾을 수 있습니다.
이상한 점은 둘 다 동일한 소스 파일과 문자 위치에서 가져온 것입니다.
브라우저가 단일 애니메이션 프레임에서 여러 키 누름을 처리하여 아무것도 페인트할 수 있기 전에 이 이벤트 리스너가 두 번 실행되었습니다.
이 효과는 이벤트 리스너가 완료하는 데 걸리는 시간이 길어질수록 더 많은 추가 입력 이벤트가 발생하여 느린 상호작용이 훨씬 더 길어질 수 있습니다.
이는 검색/자동 완성 상호작용이므로 프레임당 최대 한 번의 키 누르기가 처리되도록 입력을 디보우싱하는 것이 좋습니다.
7. 입력 지연
입력 지연(사용자가 상호작용한 시점부터 이벤트 리스너가 상호작용 처리를 시작할 수 있는 시점까지의 시간)의 일반적인 이유는 기본 스레드가 사용 중이기 때문입니다. 이 문제는 여러 가지 원인으로 인해 발생할 수 있습니다.
- 페이지가 로드되고 있으며 기본 스레드는 DOM 설정, 페이지 레이아웃 및 스타일 지정, 스크립트 평가 및 실행이라는 초기 작업을 처리하느라 바쁩니다.
- 페이지가 일반적으로 로드 중입니다(예: 계산, 스크립트 기반 애니메이션 또는 광고 실행).
- 이전 상호작용의 처리 시간이 너무 오래 걸려 향후 상호작용이 지연됩니다. 이는 마지막 예에서 확인할 수 있습니다.
데모 페이지에는 페이지 상단의 달팽이 로고를 클릭하면 애니메이션이 시작되고 과도한 기본 스레드 JavaScript 작업이 실행되는 비밀 기능이 있습니다.
- 달팽이 로고를 클릭하여 애니메이션을 시작합니다.
- JavaScript 태스크는 달팽이가 반동의 맨 아래에 있을 때 트리거됩니다. 이탈율 하단에 최대한 가깝게 페이지와 상호작용하고 얼마나 높은 INP를 트리거할 수 있는지 확인합니다.
예를 들어 달팽이가 튀어오르는 순간 검색창을 클릭하고 포커스를 맞추는 등 다른 이벤트 리스너를 트리거하지 않더라도 기본 스레드 작업으로 인해 페이지가 상당 시간 응답하지 않게 됩니다.
많은 페이지에서 과도한 메인 스레드 작업은 이렇게 잘 작동하지 않지만, INP 기여 분석 데이터에서 이를 식별하는 방법을 보여주는 좋은 예입니다.
다음은 느린 이탈 중에만 검색창에 포커스를 맞춘 기여 분석의 예입니다.
{
name: 'INP',
value: 728,
rating: 'poor',
attribution: {
interactionTargetElement: Element,
interactionTarget: '#search-terms',
interactionType: 'pointer',
inputDelay: 702.3,
processingDuration: 4.9,
presentationDelay: 20.8,
longAnimationFrameEntries: [{
name: 'long-animation-frame',
startTime: 2064.8,
duration: 790,
renderStart: 2065,
styleAndLayoutStart: 2854.2,
firstUIEventTimestamp: 0,
blockingDuration: 740,
scripts: [{...}]
}]
}
}
예상대로 이벤트 리스너가 빠르게 실행되어 처리 시간이 4.9밀리초로 표시되었으며, 상호작용의 대부분이 입력 지연으로 인해 소요되어 총 728밀리초 중 702.3밀리초가 소요되었습니다.
이 경우 디버깅하기가 어려울 수 있습니다. 사용자가 무엇과 상호작용했는지, 어떻게 상호작용했는지 알고 있지만 상호작용의 해당 부분은 빠르게 완료되었으며 문제가 되지 않았습니다. 대신 페이지에 있는 다른 요소가 상호작용 처리 시작을 지연시켰습니다. 하지만 어디에서부터 살펴봐야 할지 어떻게 알 수 있을까요?
LoAF 스크립트 항목이 문제를 해결해 줍니다.
scripts: [{
name: 'script',
invoker: 'SPAN.onanimationiteration',
invokerType: 'event-listener',
startTime: 2065,
executionStart: 2065,
duration: 788,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: 'cryptodaphneCoinHandler',
sourceCharPosition: 1831
}]
이 함수는 상호작용과 관련이 없지만 애니메이션 프레임의 속도를 느리게 했으므로 상호작용 이벤트와 조인된 LoAF 데이터에 포함됩니다.
이를 통해 상호작용 처리를 지연시킨 함수가 어떻게 (animationiteration
리스너에 의해) 트리거되었는지, 정확히 어떤 함수가 이를 담당했는지, 소스 파일에서 어디에 위치했는지 확인할 수 있습니다.
8. 프레젠테이션 지연: 업데이트가 표시되지 않는 경우
프레젠테이션 지연 시간은 이벤트 리스너가 실행을 완료한 시점부터 브라우저가 새 프레임을 화면에 페인팅하여 사용자에게 눈에 보이는 피드백을 표시할 수 있을 때까지의 시간을 측정합니다.
페이지를 새로고침하여 INP 값을 다시 재설정한 다음 햄버거 메뉴를 엽니다. 열 때 확실히 문제가 있습니다.
어떤 모습일까요?
{
name: 'INP',
value: 376,
rating: 'needs-improvement',
delta: 352,
attribution: {
interactionTarget: '#sidenav-button>svg',
interactionType: 'pointer',
inputDelay: 12.8,
processingDuration: 14.7,
presentationDelay: 348.5,
longAnimationFrameEntries: [{
name: 'long-animation-frame',
startTime: 651,
duration: 365,
renderStart: 673.2,
styleAndLayoutStart: 1004.3,
firstUIEventTimestamp: 138.6,
blockingDuration: 315,
scripts: [{...}]
}]
}
}
이번에는 느린 상호작용의 대부분을 차지하는 것이 표시 지연입니다. 즉, 기본 스레드를 차단하는 항목은 이벤트 리스너가 완료된 후에 발생합니다.
scripts: [{
entryType: 'script',
invoker: 'FrameRequestCallback',
invokerType: 'user-callback',
startTime: 673.8,
executionStart: 673.8,
duration: 330,
sourceURL: 'http://localhost:8080/js/side-nav.js',
sourceFunctionName: '',
sourceCharPosition: 1193,
}]
scripts
배열의 단일 항목을 보면 FrameRequestCallback
의 user-callback
에서 시간이 소요된 것을 확인할 수 있습니다. 이번에는 requestAnimationFrame
콜백으로 인해 프레젠테이션이 지연되었습니다.
9. 결론
필드 데이터 집계
단일 페이지 로드에서 단일 INP 기여 분석 항목을 보면 이 모든 작업이 더 쉬워집니다. 현장 데이터를 기반으로 INP를 디버그하기 위해 이 데이터를 집계하려면 어떻게 해야 하나요? 사실 유용한 세부정보가 많을수록 더 어려워집니다.
예를 들어 상호작용이 느려지는 일반적인 원인이 되는 페이지 요소를 파악하는 것은 매우 유용합니다. 그러나 페이지에 빌드마다 변경되는 컴파일된 CSS 클래스 이름이 있는 경우 동일한 요소의 web-vitals
선택자가 빌드마다 다를 수 있습니다.
대신 특정 애플리케이션을 고려하여 가장 유용한 항목과 데이터를 집계하는 방법을 결정해야 합니다. 예를 들어 기여 분석 데이터를 다시 비콘으로 전송하기 전에 타겟이 있는 구성요소 또는 타겟이 실행하는 ARIA 역할을 기반으로 web-vitals
선택기를 자체 식별자로 대체할 수 있습니다.
마찬가지로 scripts
항목의 sourceURL
경로에 파일 기반 해시가 포함되어 있어 결합하기 어려울 수 있지만, 데이터를 다시 비콘하기 전에 알려진 빌드 프로세스에 따라 해시를 삭제할 수 있습니다.
안타깝게도 이처럼 복잡한 데이터를 처리하는 쉬운 방법은 없지만, 디버깅 프로세스에서 기여 분석 데이터를 전혀 사용하지 않는 것보다는 하위 집합을 사용하는 것이 더 가치가 있습니다.
어디서나 저작자 표시
LoAF 기반 INP 기여 분석은 강력한 디버깅 도구입니다. INP 중에 구체적으로 발생한 상황에 관한 세부적인 데이터를 제공합니다. 많은 경우 최적화 작업을 시작해야 하는 스크립트의 정확한 위치를 알려줄 수 있습니다.
이제 모든 사이트에서 INP 기여 분석 데이터를 사용할 수 있습니다.
페이지를 수정할 수 있는 액세스 권한이 없더라도 DevTools 콘솔에서 다음 스니펫을 실행하여 이 Codelab의 프로세스를 재현하고 어떤 결과가 나오는지 확인할 수 있습니다.
const script = document.createElement('script');
script.src = 'https://unpkg.com/web-vitals@4/dist/web-vitals.attribution.iife.js';
script.onload = function () {
webVitals.onINP(console.log, {reportAllChanges: true});
};
document.head.appendChild(script);