- Published on
Chrome INP 폭증 원인 찾기 - Long Task·Event Timing
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 지표는 멀쩡한데(응답 시간, 에러율 변화 없음) 갑자기 Chrome에서만 INP(Interaction to Next Paint)가 폭증하는 경우가 있습니다. 이런 케이스는 대개 네트워크가 아니라 메인 스레드(Main thread) 점유 문제로 귀결됩니다. 특히 다음 두 축을 놓치면 원인 규명이 길어집니다.
- Long Task(50ms 이상 메인 스레드 작업): 렌더/스크립트/레이아웃/GC 등으로 이벤트 처리와 페인트가 밀림
- Event Timing(느린 입력 이벤트의 실제 처리 시간): 어떤 사용자 입력이, 어디서, 얼마나 지연됐는지 “상호작용 단위”로 추적
이 글은 “INP가 왜 튀었는지”를 재현 → 수집 → 상관관계 분석 → 코드 수준 수정까지 이어지는 체크리스트로 정리합니다.
> 참고로 브라우저 렌더링/컴포지팅 이슈를 파고드는 관점은 Safari 스크롤 잔상? Compositor 레이어 디버깅에서도 유사하게 적용됩니다. INP도 결국 ‘언제 메인 스레드가 막혔나’를 찾는 게임입니다.
INP 폭증을 “Long Task + Event Timing”으로 보는 이유
INP는 사용자의 상호작용(클릭/탭/키 입력 등)부터 다음 페인트까지의 지연을 대표값으로 잡습니다. 즉, 아래 중 하나만 발생해도 INP는 크게 튈 수 있습니다.
- 이벤트 핸들러 자체가 오래 걸림(무거운 JS)
- 이벤트는 빨리 끝났지만, 그 뒤 레이아웃/스타일 계산/페인트가 밀림
- 이벤트가 큐에 들어갔는데, 앞에 다른 작업(Long Task)이 길게 점유해서 시작이 늦음
여기서 Long Task는 ‘왜 이벤트가 늦게 시작/완료됐는지’의 공통 원인이 되고, Event Timing은 ‘어떤 이벤트가 INP를 만들었는지’의 증거가 됩니다.
1) DevTools에서 빠르게 범인 좁히기 (Performance 패널)
재현 환경을 먼저 고정하기
INP 폭증은 “특정 기기/특정 입력/특정 화면”에서만 터지는 경우가 많습니다.
- Chrome 버전(Stable/Canary), OS, 디바이스(모바일/데스크탑)
- 입력 종류(클릭 vs 키입력 vs 터치)
- 페이지 상태(초기 로딩 직후? 스크롤 후? 모달 열린 상태?)
가능하면 CPU 4x/6x throttling을 켜서 재현성을 올립니다.
Performance 기록 시 반드시 볼 것
- Main 트랙에서 50ms 이상 긴 막대(= Long Task)
- 해당 구간에 Event(click, pointerdown, keydown 등)가 끼어 있는지
- Long Task의 원인이 Scripting / Rendering / Painting / System / GC 중 무엇인지
흔한 패턴
- Scripting이 길다: 대량 JSON 파싱, 정렬/필터, 큰 루프, 동기식 상태 계산, 불필요한 리렌더
- Rendering/레이아웃이 길다: DOM 대량 변경, 레이아웃 스래싱(읽기/쓰기 교차), 큰 테이블
- GC가 튄다: 이벤트마다 객체를 과도 생성(배열/클로저), 큰 문자열 조작
“왜 Chrome에서만?”의 단서
Chrome의 스케줄링/최적화 특성, 특정 API 구현 차이, 확장 프로그램, 폰트/캔버스/필터 렌더링 경로 차이로 인해 “Chrome에서만” 더 크게 드러날 수 있습니다. 따라서 Chrome Performance에서 Long Task를 먼저 잡고, 같은 상호작용을 다른 브라우저와 비교해 차이를 확인하는 게 빠릅니다.
2) Event Timing으로 ‘INP를 만든 상호작용’을 특정하기
DevTools만으로도 많은 걸 보지만, 운영 환경에서 “어떤 이벤트가 INP를 만들었는지”를 자동 수집하려면 Event Timing API가 핵심입니다.
Event Timing 관측 코드(운영 계측)
아래 코드는 느린 이벤트를 콘솔로 찍는 예시입니다. 실제로는 로그/분석 파이프라인으로 전송하면 됩니다.
// 느린 입력 이벤트를 수집하는 최소 예시
// Chrome에서 Event Timing이 지원되며, entry.duration이 핵심 단서가 됩니다.
const SLOW_EVENT_THRESHOLD = 100; // ms
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.name: 'click', 'pointerdown', 'keydown' 등
// entry.duration: 이벤트 처리 + 다음 페인트까지 영향(브라우저 구현에 따라 세부값은 다를 수 있음)
if (entry.duration >= SLOW_EVENT_THRESHOLD) {
console.log('[Slow Event]', {
name: entry.name,
duration: entry.duration,
startTime: entry.startTime,
processingStart: entry.processingStart,
processingEnd: entry.processingEnd,
interactionId: entry.interactionId,
target: entry.target ? entry.target.tagName : undefined,
});
}
}
});
po.observe({ type: 'event', buffered: true, durationThreshold: SLOW_EVENT_THRESHOLD });
읽는 법(실전 포인트)
startTime → processingStart가 크면: 이벤트가 큐에서 대기(앞에 Long Task가 막고 있었을 확률)processingStart → processingEnd가 크면: 핸들러/동기 작업이 무거움processingEnd → 다음 페인트가 크면(간접적으로 duration에 반영): 레이아웃/페인트/스타일 계산이 무거움
그리고 interactionId를 키로 묶으면 “한 번의 상호작용”을 더 정확히 추적할 수 있습니다.
3) Long Task를 코드로 잡아내기 (Long Tasks API)
Performance 패널에서 Long Task가 보이는데, 운영에서 어떤 코드 경로가 원인인지 애매할 때가 많습니다. 그럴 땐 Long Tasks API를 함께 씁니다.
// Long Task 감지: 50ms 이상 메인 스레드 점유
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('[Long Task]', {
duration: entry.duration,
startTime: entry.startTime,
name: entry.name,
attribution: entry.attribution?.map(a => ({
name: a.name,
entryType: a.entryType,
containerType: a.containerType,
containerSrc: a.containerSrc,
}))
});
}
});
longTaskObserver.observe({ type: 'longtask', buffered: true });
이 데이터는 Event Timing과 합쳐서 보면 효과가 큽니다.
- 특정 클릭에서 INP가 튐 → 같은 시점에 Long Task 존재 → 그 Long Task 직전에 어떤 라우트/컴포넌트/핸들러가 실행됐는지 추적
4) 흔한 원인별 “패턴 인식” 체크리스트
(1) 클릭 시 setState 폭발/리렌더 폭발
- 리스트/테이블 전체가 리렌더
- memoization 부재
- 상태가 상위로 과도하게 끌어올려짐
대응
- 렌더 범위 최소화(컴포넌트 분리)
memo,useMemo,useCallback의 목표 지점 적용(무분별 적용 금지)- 가상 스크롤(대량 DOM)
(2) 레이아웃 스래싱
패턴
- 이벤트 핸들러에서
getBoundingClientRect()같은 레이아웃 읽기와style.width = ...같은 쓰기가 교차
대응
- 읽기 → 쓰기 순서로 배치
- 측정값 캐싱
requestAnimationFrame으로 DOM 쓰기를 프레임 경계로 밀기
button.addEventListener('click', () => {
// 읽기
const rect = panel.getBoundingClientRect();
// 쓰기는 다음 프레임에
requestAnimationFrame(() => {
panel.style.height = `${rect.height + 40}px`;
});
});
(3) 무거운 동기 작업(정렬/필터/파싱)이 이벤트에 직결
대응 전략은 “쪼개기/오프로딩”입니다.
- 작업을 청크로 나누고
setTimeout(0)/requestIdleCallback로 분산 - 가능하면 Web Worker로 이동
// 큰 작업을 청크로 쪼개 메인 스레드 점유를 줄이는 예
function chunkedProcess(items, chunkSize = 500) {
let i = 0;
return new Promise((resolve) => {
function run() {
const end = Math.min(i + chunkSize, items.length);
for (; i < end; i++) {
// 무거운 계산
items[i].computed = heavy(items[i]);
}
if (i < items.length) {
setTimeout(run, 0);
} else {
resolve(items);
}
}
run();
});
}
(4) 서드파티 스크립트(광고/태그/위젯)로 인한 Long Task
- 특정 페이지/특정 사용자 세그먼트에서만 INP 폭증
- Performance에서 호출 스택이 외부 스크립트로 이어짐
대응
- 지연 로딩(상호작용 이후 로드)
- 우선순위 낮추기(
defer,async, 동적 import) - 필요 시 격리(iframe, worker)
(5) 메인 스레드가 “작업 큐”로 막히는 구조적 문제
- 스크롤/입력과 무관한 주기 작업(setInterval)로 계속 바쁨
- 애니메이션을 JS 타이머로 구현
대응
- CSS 애니메이션/컴포지터 친화 속성(transform/opacity) 활용
- 폴링 제거, 이벤트 기반으로 전환
5) DevTools에서 ‘이벤트-롱태스크-페인트’ 연결하기
실무에서 가장 시간을 절약하는 방법은 한 번의 느린 상호작용을 기준으로 타임라인을 묶어서 읽는 것입니다.
- Event Timing으로 느린
interactionId/이벤트 종류를 특정 - 해당 시점의 Performance 트레이스에서 Main thread Long Task 확인
- Long Task 내부에서 무슨 일이 있었는지(스크립팅/레이아웃/GC) 분류
- 원인 코드 경로를 찾고(컴포넌트/함수/서드파티) 재현 케이스로 수정 검증
이때 “캐시/상태 꼬임”이 원인처럼 보이는 프런트 이슈도 종종 섞입니다. 특히 Next.js App Router에서 캐시/데이터 동기화가 어긋나면 불필요한 렌더/재요청으로 메인 스레드가 바빠질 수 있어, 유사 증상일 땐 Next.js 14 App Router 캐시 꼬임 해결법도 같이 점검해볼 만합니다.
6) “수정했는데도 INP가 들쭉날쭉”할 때 보는 것
샘플링/대표값의 함정
INP는 단일 이벤트가 아니라 “세션 내 상호작용” 중 대표값을 사용합니다. 특정 사용자만 겪는 극단값이 전체 지표를 흔들 수 있습니다.
- 특정 입력(예: 첫 클릭)만 느린지
- 특정 기능(필터/정렬/검색)에서만 느린지
- 특정 브라우저 확장/번역/보안 제품이 개입하는지
관측 오버헤드 주의
PerformanceObserver로 과도한 로깅을 하면 오히려 메인 스레드를 압박할 수 있습니다.
- 임계치 이상만 수집
- 샘플링(예: 1~5%)
- 전송은
navigator.sendBeacon또는 배치 전송
function report(payload) {
const body = JSON.stringify(payload);
// 페이지 이동/종료에도 유실이 적음
navigator.sendBeacon('/rum', body);
}
7) 실전 결론: INP 폭증 대응 플레이북
정리하면, Chrome INP 폭증은 대부분 아래 순서로 해결 속도가 빨라집니다.
- 재현 조건 고정(기기/화면/입력)
- Performance에서 Long Task 위치를 찾고 원인 분류(Scripting/Rendering/GC)
- Event Timing으로 어떤 이벤트가 문제인지 특정(큐 대기 vs 핸들러 vs 렌더)
- 코드 수정은 “작업 쪼개기/렌더 범위 축소/레이아웃 스래싱 제거/서드파티 지연”의 우선순위로 적용
- 운영 계측으로 재발 방지(느린 이벤트/롱태스크 자동 수집)
INP는 최적화의 끝이 아니라, 사용자 경험을 수치로 드러내는 경보 장치에 가깝습니다. Long Task와 Event Timing을 같이 보면 ‘감’이 아니라 증거 기반으로 원인을 좁힐 수 있고, 팀 내 커뮤니케이션도 “어느 클릭에서 무엇이 300ms를 먹었는지”로 정교해집니다.