INP
고친 사람 github-actions[bot]
INP 는 웹 페이지가 사용자의 클릭이나 키 입력에 얼마나 빨리 답하는지를 잽니다. 버튼을 누른 뒤 화면이 바뀌어 보이기까지 걸린 시간입니다. 페이지에 머무는 동안 가장 굼떴던 반응이 그 페이지의 값이 됩니다. 값이 작을수록 빨리 답하는 페이지입니다.
쉽고 빠른 이해
INP 는 누른 뒤 화면이 답하기까지 걸린 시간을 알려 줍니다. 메뉴 버튼을 눌렀는데 0.5초 넘게 메뉴가 안 펼쳐지면 이 값이 나빠집니다.
이 값이 없으면 「버튼이 먹통 같다」는 불만을 숫자로 견줄 수 없습니다. 페이지가 금방 떠도 뜬 뒤에 굼뜨면 사용자는 같은 버튼을 몇 번이고 다시 누릅니다.
어떻게 도나:
- 클릭 · 탭 · 키 입력이 있을 때마다 화면이 다음으로 바뀌기까지의 시간을 잽니다
- 페이지에 머무는 동안 잰 값을 모읍니다
- 그중 가장 오래 걸린 값을 고릅니다. 아주 많이 누른 방문이면 튄 값 몇 개를 빼고 고릅니다
200밀리초 이하면 빨리 답하는 페이지로 칩니다. 값을 낮추려면 오래 도는 스크립트를 잘게 나눠야 해서 코드가 복잡해집니다.
재는 데도 한계가 있습니다. 서버 응답을 기다리는 시간은 재지 않습니다. 한 번도 누르지 않은 방문에서는 값이 아예 나오지 않습니다.
상세
이 절은 먼저 INP 라는 이름을 풉니다. 이어서 반응 시간 한 번이 어떤 구간으로 이루어지는지 예 하나로 계산해 봅니다. 그다음 많은 반응 가운데 값 하나를 고르는 법과 기준값을 다룹니다.
뒤쪽은 재는 법과 줄이는 법입니다. 백엔드 개발자가 손댈 수 있는 곳도 거기서 짚습니다. 마지막 소절은 INP 가 재지 않는 것입니다.
엘리베이터 버튼
엘리베이터 버튼을 눌렀는데 불이 안 들어오면 사람은 몇 번이고 다시 누릅니다. 불이 바로 들어오면 엘리베이터가 늦게 와도 기다립니다. 눌렸다는 것을 알았기 때문입니다.
웹 페이지의 버튼도 같습니다. 누른 뒤 화면에 아무 변화가 없으면 사용자는 눌린 줄 모릅니다. INP 는 그 「불이 들어오기까지」의 시간을 잽니다.
이름이 말하는 것
INP 는 Interaction to Next Paint 의 줄임말입니다. 우리말로 옮기면 다음 페인트까지의 상호작용입니다.
상호작용은 사용자가 페이지에 무언가를 해 달라고 건네는 동작입니다. 앞에서 「누른다」고 적은 것이 이것입니다. 마우스 클릭, 터치 화면의 탭, 키보드 입력 셋을 셉니다. 화면에 뜨는 가상 키보드의 입력도 키 입력에 듭니다.
마우스를 올려 두기만 하는 것, 스크롤, 두 손가락 확대는 셈에 넣지 않습니다.
페인트는 브라우저가 화면에 픽셀을 칠하는 일입니다. 사용자는 칠해진 것만 볼 수 있습니다. 누른 결과도 칠해져야 비로소 눈에 들어옵니다.
브라우저는 화면을 1초에도 여러 번 새로 칠합니다. 새로 칠한 화면 한 장이 프레임입니다.
「다음 페인트」는 상호작용을 처리한 뒤 처음 칠하는 프레임입니다. 누른 결과가 처음 보일 수 있는 순간이라 여기서 재기를 멈춥니다.
반응을 맡는 메인 스레드
반응 시간을 쪼개 보려면 먼저 누가 반응을 맡는지 알아야 합니다. 브라우저는 페이지의 자바스크립트 실행과 화면 그리기를 한 스레드에 맡깁니다. 이 스레드가 메인 스레드입니다.
메인 스레드는 한 번에 한 가지 일만 합니다. 할 일은 줄을 서서 하나씩 차례로 처리됩니다. 화면 칠하기는 앞의 일이 끝난 뒤, 다음 일로 넘어가기 전에 끼어듭니다. 그래서 스크립트가 도는 동안에는 화면을 칠하지 못합니다.
클릭이 들어오면 메인 스레드가 이벤트 핸들러를 부릅니다. 이벤트 핸들러는 「버튼이 눌리면 이걸 해라」 하고 미리 걸어 둔 함수입니다. 장바구니 담기 버튼이라면 개수를 올리고 화면의 숫자를 바꾸는 함수가 걸려 있습니다.
반응 시간을 이루는 세 구간
반응 시간 한 번은 세 구간을 이어 붙인 것입니다. 아래 그림은 버튼을 누른 때부터 다음 프레임이 칠해질 때까지를 순서대로 그린 것입니다.
sequenceDiagram
participant 사용자
participant 메인 스레드
participant 화면
사용자->>메인 스레드: 버튼을 누른다
Note over 메인 스레드: 하던 일을 마저 끝낸다 · 입력 지연
Note over 메인 스레드: 이벤트 핸들러를 돌린다 · 처리 시간
Note over 메인 스레드: 칠할 준비를 한다 · 표시 지연
메인 스레드->>화면: 다음 프레임을 칠한다
Note over 사용자,화면: 누른 때부터 여기까지가 반응 시간
첫 구간은 입력 지연(input delay)입니다. 클릭이 들어왔을 때 메인 스레드가 다른 일을 하고 있으면 그 일이 끝날 때까지 기다립니다. 메인 스레드가 놀고 있었다면 이 구간은 거의 없습니다.
둘째 구간은 처리 시간(processing duration)입니다. 그 클릭에 걸린 이벤트 핸들러들이 모두 도는 시간입니다. 핸들러의 계산이 오래 걸리면 이 구간이 길어집니다.
셋째 구간은 표시 지연(presentation delay)입니다. 핸들러가 끝난 뒤 다음 프레임이 칠해지기까지의 시간입니다. 브라우저는 이 사이에 칠할 준비를 합니다.
칠할 준비 가운데 큰 일이 레이아웃입니다. 레이아웃은 버튼·글상자 같은 화면 요소의 위치와 크기를 다시 잡는 일입니다. 핸들러가 한 번에 많은 요소를 바꿔 두면 레이아웃이 오래 걸립니다.
세 구간을 숫자로 이어 보면 이렇습니다. 메인 스레드가 하던 일을 120밀리초 더 했습니다. 핸들러는 60밀리초 돌았습니다. 칠할 준비에는 40밀리초가 걸렸습니다.
입력 지연 120밀리초
처리 시간 60밀리초
표시 지연 40밀리초
반응 시간 120 + 60 + 40 // 220밀리초
핸들러 자신은 60밀리초밖에 안 돌았습니다. 그런데 사용자는 220밀리초를 기다렸습니다. 가장 긴 구간이 핸들러가 아니라 앞서 돌던 다른 일이었습니다.
한 번 누르는 데도 이벤트는 여럿 생깁니다. 손가락이 닿는 순간, 떨어지는 순간, 그리고 클릭이 따로 들어옵니다. 그중 가장 오래 걸린 이벤트의 시간이 그 상호작용의 반응 시간이 됩니다.
긴 작업이 입력을 막는다
입력 지연이 길어지는 흔한 원인은 긴 작업(long task)입니다. 긴 작업은 메인 스레드를 50밀리초 넘게 붙잡는 한 덩어리의 일입니다. 메인 스레드는 한 덩어리를 시작하면 끝날 때까지 다른 일로 넘어가지 않습니다.
예를 들어 페이지가 API(Application Programming Interface) 응답으로 받은 큰 JSON(JavaScript Object Notation)을 풀어 수천 줄짜리 표를 그린다고 합시다. 이 일이 한 덩어리로 300밀리초 걸립니다. 그 사이에 사용자가 버튼을 누르면 클릭은 300밀리초가 끝날 때까지 줄을 서 있습니다.
사용자 쪽에서는 누른 시점이 운입니다. 긴 작업이 막 시작된 때 누르면 오래 기다립니다. 끝날 무렵 누르면 금방 답이 옵니다. 같은 페이지도 방문마다 INP 가 다르게 나오는 까닭입니다.
페이지 하나에 값 하나를 고르는 법
INP 는 페이지에 머무는 동안 일어난 상호작용을 모두 잽니다. 그리고 그 가운데 하나를 골라 그 방문의 값으로 삼습니다.
고르는 규칙은 가장 오래 걸린 값입니다. 평균을 쓰면 금방 끝난 반응 여럿이 굼뜬 한 번을 가립니다. 사용자가 겪은 가장 긴 기다림이 드러나도록 맨 끝의 값을 고릅니다.
다만 상호작용이 아주 많으면 맨 끝 값은 우연히 튄 한 번일 수 있습니다. 이를 막으려고 상호작용 50번마다 가장 오래 걸린 것을 하나씩 빼고 고릅니다.
| 한 방문의 상호작용 수 | INP 로 고르는 값 |
|---|---|
| 0번 | 값이 없다 |
| 1 ~ 49번 | 가장 오래 걸린 값 |
| 120번 | 세 번째로 오래 걸린 값 |
120번이면 50번 단위가 두 번 차므로 가장 오래 걸린 둘을 뺍니다. 남은 것 가운데 가장 오래 걸린 값, 곧 세 번째로 오래 걸린 값이 INP 가 됩니다. 대부분의 방문은 50번에 못 미쳐서 가장 오래 걸린 값이 그대로 쓰입니다.
기준값
INP 는 시간이라 단위가 밀리초입니다. 기준값은 셋으로 갈립니다.
| INP | 판정 이름 | 뜻 |
|---|---|---|
| 200밀리초 이하 | good | 빨리 답한다 |
| 200밀리초 초과 500밀리초 이하 | needs improvement | 손볼 만하다 |
| 500밀리초 초과 | poor | 굼뜨다 |
방문 한 번의 값으로 판정하지 않습니다. 앞에서 본 것처럼 누른 시점과 기기에 따라 방문마다 값이 다르기 때문입니다. 방문들의 INP 를 작은 것부터 줄 세웠을 때 75% 지점에 오는 값, 곧 75백분위 값으로 판정합니다.
75백분위가 200밀리초 이하라는 말은 방문 넷 중 셋이 200밀리초 안에 답했다는 뜻입니다. 휴대폰과 데스크톱은 처리 능력이 달라서 따로 잽니다.
코어 웹 바이탈의 세 지표
INP 는 구글이 웹 페이지의 사용자 경험을 재려고 고른 코어 웹 바이탈(Core Web Vitals) 세 지표 가운데 하나입니다. 셋은 방문 한 번 안에서 서로 다른 불편을 잽니다.
| 지표 | 재는 것 | 단위 |
|---|---|---|
| LCP(Largest Contentful Paint) | 가장 큰 내용이 화면에 뜨기까지 걸린 시간 | 시간 |
| INP | 누른 뒤 화면이 반응하기까지 걸린 시간 | 시간 |
| CLS(Cumulative Layout Shift) | 화면이 밀려난 정도 | 없음 |
LCP 는 페이지가 뜨는 순간을 다룹니다. INP 와 CLS 는 뜬 뒤 머무는 동안을 다룹니다. 금방 뜨는 페이지도 뜬 뒤에 굼뜨면 INP 값이 커집니다.
FID 를 대신한 까닭
INP 전에는 FID(First Input Delay, 첫 입력 지연)가 반응을 쟀습니다. FID 는 첫 상호작용 하나만 쟀습니다. 그것도 세 구간 가운데 입력 지연만 셌습니다.
페이지가 뜬 직후의 첫 클릭만 보면 그 뒤의 굼뜬 반응을 놓칩니다. 핸들러가 무거워서 늦는 경우도 FID 에는 안 잡힙니다. INP 는 머무는 내내의 상호작용을, 다음 프레임이 칠해질 때까지 잽니다.
INP 는 2024년 3월에 코어 웹 바이탈에 들었습니다. 그때부터 FID 를 대신합니다.
재는 법
INP 는 진짜 사용자가 눌러야 생기는 값입니다. 방문자의 브라우저에서 잰 값이 판정의 근거가 됩니다. 이렇게 방문자 쪽에서 재서 서버로 모으는 방식이 실사용자 모니터링(Real User Monitoring, RUM)입니다.
브라우저는 이벤트마다 걸린 시간을 페이지 안의 자바스크립트에 알려 줍니다. 받는 통로가 PerformanceObserver 입니다. 아래 코드는 앞의 예처럼 220밀리초쯤 걸린 클릭이 들어오면 그 시간을 찍습니다.
const po = new PerformanceObserver((l) => {
for (const e of l.getEntries()) {
if (!e.interactionId) continue;
console.log(e.duration); // 216
}
});
po.observe({ type: "event", durationThreshold: 16 });
찍히는 값은 앞에서 계산한 220 이 아니라 216 입니다. 브라우저가 걸린 시간을 정확히 주지 않고 어림해서 알려 주기 때문입니다.
interactionId 는 이벤트가 어느 상호작용에 속하는지를 알려 주는 번호입니다. 번호가 없는 이벤트는 셈에 안 드는 동작이라 건너뜁니다. 같은 번호끼리 묶어 가장 긴 값을 고르면 상호작용 하나의 반응 시간이 나옵니다.
durationThreshold 는 이 시간보다 짧은 이벤트는 알리지 말라는 문턱입니다. 따로 적지 않으면 104밀리초보다 짧은 이벤트는 알려 오지 않습니다. 가장 낮게 잡을 수 있는 값이 16밀리초입니다.
자동으로 페이지를 열어 성능을 재는 시험용 도구로는 INP 를 재기 어렵습니다. 페이지를 열어 보기만 하고 아무도 누르지 않기 때문입니다. Lighthouse 가 이런 도구입니다.
이런 도구는 INP 대신 TBT(Total Blocking Time, 총 차단 시간)를 잽니다. TBT 는 페이지를 여는 동안 긴 작업이 메인 스레드를 막은 시간을 더한 값입니다. 누르는 사람이 없어도 클릭이 막힐 만한 시간을 짐작할 수 있습니다.
줄이는 법
줄이는 방법은 하나로 모입니다. 메인 스레드를 오래 붙잡지 않는 것입니다. 메인 스레드가 자주 비어 있으면 클릭이 들어오자마자 처리할 수 있습니다.
| 반응을 늦추는 것 | 막는 법 |
|---|---|
| 한 덩어리로 오래 도는 스크립트 | 잘게 나눠 중간중간 메인 스레드를 내줌 |
| 오래 걸리는 일을 다 끝내고 나서야 화면을 바꾸는 핸들러 | 화면부터 바꾸고 오래 걸리는 일은 다음 차례로 미룸 |
| 한 번에 너무 많은 요소를 바꾸는 핸들러 | 바꾸는 범위를 줄임 |
| 한 번에 너무 큰 응답 | 나눠 받거나 필요한 만큼만 받음 |
표의 둘째 줄을 코드로 옮기면 이렇습니다. 처리 중 표시를 먼저 띄웁니다. 오래 걸리는 일은 다음 차례로 넘깁니다.
button.addEventListener("click", () => {
showSpinner(); // 먼저 칠한다
setTimeout(heavyWork, 0); // 다음 차례로
});
setTimeout 에 0 을 주면 그 함수를 지금 말고 다음 차례에 돌리라는 뜻입니다. 핸들러가 바로 끝나므로 브라우저는 heavyWork 차례가 오기 전에 처리 중 표시를 칠할 틈을 얻습니다. 반응 시간은 그 프레임에서 끝납니다.
표의 넷째 줄이 백엔드와 맞닿습니다. 목록 API 가 수천 건을 한 번에 내려보내면 브라우저가 그것을 풀고 그리느라 메인 스레드가 묶입니다. 응답을 페이지네이션으로 나눠 보내면 한 번에 묶이는 시간이 짧아집니다.
서버가 내용을 채운 HTML(HyperText Markup Language)을 보내는 페이지도 살펴볼 곳이 있습니다. 화면은 먼저 보이지만 버튼은 스크립트가 붙고 나서야 동작합니다. 서버가 그려 보낸 화면에 브라우저가 동작을 붙이는 이 일이 하이드레이션입니다. 하이드레이션이 길게 돌면 그 사이의 클릭이 입력 지연으로 쌓입니다.
INP 가 재지 않는 것
INP 는 클릭 뒤 서버 응답을 기다리는 시간을 재지 않습니다. 핸들러가 요청만 보내고 끝나면 그 뒤 처음 칠한 프레임에서 재기가 끝납니다. 응답이 3초 뒤에 와도 INP 는 짧게 나올 수 있습니다.
INP 가 좋아도 사용자는 여전히 기다릴 수 있습니다. 응답이 늦는 API 는 서버 쪽 응답 시간으로 따로 재야 합니다. 누르자마자 처리 중 표시를 칠해 두면 사용자는 적어도 눌렸다는 것을 압니다.
INP 는 가장 오래 걸린 반응 하나로 정해집니다. 대부분 금방 답하고 한 번만 오래 걸린 페이지와, 전부 조금씩 늦는 페이지가 같은 값을 받을 수 있습니다. 그리고 한 번도 누르지 않은 방문은 값이 없어서 통계에 안 들어갑니다.
INP 는 브라우저 안에서만 뜻이 있습니다. 휴대폰 앱의 네이티브 화면이나 API 서버 자체에는 붙지 않습니다.
관련 항목
INP 를 이루는 반응 시간의 조각
상호작용 · 입력 지연 · 이벤트 핸들러 · 이벤트 · Event Timing API · interactionId
INP 와 함께 사용자 경험을 재는 지표
코어 웹 바이탈 · LCP · CLS · FID · TBT · FCP · TTFB · Web Vitals
INP 를 재고 모으는 도구와 방식
PerformanceObserver · 실사용자 모니터링 · Lighthouse · CrUX · 백분위수
INP 를 늘리는 브라우저의 처리 단계
메인 스레드 · 긴 작업 · 이벤트 루프 · JavaScript · 레이아웃 · 페인트 · 프레임 · DOM · 하이드레이션
INP 를 낮추는 기법
setTimeout · scheduler.yield · 웹 워커 · 디바운스 · 페이지네이션 · 가상 스크롤 · 낙관적 UI
INP 가 속하는 상위 분류
다른 이름: Interaction to Next Paint · 다음 페인트까지의 상호작용