- Published on
Chrome INP 급락 원인 찾기 - Long Task·TBT 분석
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 응답도 빠르고, CLS도 안정적인데 체감이 갑자기 뻣뻣해졌다면 대개 INP(Interaction to Next Paint) 가 급락한 상황입니다. INP는 사용자의 클릭·탭·키 입력 같은 상호작용 이후 다음 페인트까지 걸린 시간을 보며, 실제로는 “입력 이벤트가 제때 처리되지 못하고 밀리는가”를 드러냅니다.
INP가 악화될 때 현장에서 가장 자주 보는 패턴은 단순합니다.
- 메인 스레드에서 Long Task 가 늘어남
- 그 결과 TBT(Total Blocking Time) 가 증가함
- 이벤트 핸들러 자체가 느리거나, 핸들러는 짧아도 직전·직후에 메인이 막혀 입력 처리 시작이 지연됨
이 글은 “왜 INP가 떨어졌는지”를 감으로 추측하는 대신, Chrome DevTools로 Long Task·TBT를 증거 기반으로 역추적해 원인을 특정하는 방법을 다룹니다.
참고로, 성능 이슈도 장애 분석처럼 “재현, 로그, 상관관계, 가설 검증” 순서로 접근하면 훨씬 빨리 결론에 도달합니다. 장애 원인 추적 관점은 아래 글도 함께 보면 도움이 됩니다.
INP, Long Task, TBT를 한 번에 정리
INP가 실제로 측정하는 것
INP는 상호작용 하나에 대해 대략 다음 시간을 포괄합니다.
- 입력 발생
- 이벤트 큐에서 해당 이벤트가 실행될 차례가 올 때까지의 지연(메인이 바쁘면 여기서 밀림)
- 이벤트 핸들러 실행 시간
- 렌더링 및 다음 페인트
즉, 핸들러가 짧더라도 메인 스레드가 다른 일로 막혀 있으면 INP가 나빠질 수 있습니다.
Long Task
Long Task는 메인 스레드에서 50ms를 초과해 실행된 작업을 의미합니다. 50ms가 넘는 순간부터는 사용자 입력이 “즉시 반응”으로 느껴지기 어렵고, 이벤트 처리 지연이 눈에 띄게 늘어납니다.
TBT
TBT는 페이지 로딩 구간에서 Long Task가 만들어낸 “차단 시간”을 합산한 지표입니다. 계산은 다음 개념으로 이해하면 됩니다.
- Long Task 시간 중 50ms를 초과한 부분이 blocking time
- 그 blocking time을 합친 것이 TBT
예를 들어 200ms짜리 Long Task 하나는 blocking time이 150ms가 됩니다.
중요한 포인트는, TBT는 로딩 구간 지표이고 INP는 상호작용 지표라는 점입니다. 다만 원인(메인 스레드 과점유)이 겹치기 때문에 DevTools에서 Long Task를 잡아내면 INP도 함께 개선되는 경우가 많습니다.
“INP가 급락했다”를 재현 가능한 문제로 바꾸기
INP 문제는 “특정 유저가 느끼다”로 시작하면 끝이 없습니다. 먼저 재현성을 확보해야 합니다.
1) 필드 데이터에서 하락 구간을 특정
가능하면 RUM(예: web-vitals, Datadog RUM, New Relic Browser, GA4 커스텀 이벤트 등)에서 다음을 확인합니다.
- INP 악화가 시작된 배포 버전
- 특정 라우트에서만 발생하는지
- 특정 디바이스(저사양 Android)에서만 심한지
- 특정 상호작용(검색 입력, 필터 토글, 장바구니 추가 등)에서만 발생하는지
필드에서 “어떤 액션이 문제인지”가 잡히면, 랩에서 DevTools로 같은 액션을 반복해 성능 트레이스를 뜰 수 있습니다.
2) 랩 환경에서 CPU 쓰로틀링을 켜고 재현
고성능 맥북에서만 측정하면 Long Task가 숨습니다.
- DevTools
Performance탭 CPU를4x또는6xslowdown- 가능하면 시크릿 모드로 확장 프로그램 영향 최소화
이 상태에서 문제 상호작용을 3회 이상 반복하고 트레이스를 저장합니다.
DevTools Performance로 Long Task 원인을 “코드 라인”까지 찾기
1) Performance 프로파일링 절차
- DevTools 열기
Performance탭Record시작- 문제 상호작용 수행(예: 버튼 클릭, 입력, 스크롤)
Stop
이후 타임라인에서 다음을 봅니다.
- Main 스레드의 긴 노란색(스크립팅) 구간
Long task마커Event(click, input, keydown 등) 처리 구간이 밀리는지
2) Bottom-Up, Call Tree로 “무엇이 CPU를 먹는지” 확인
Bottom-Up 뷰는 “가장 비용이 큰 함수”부터 보여줍니다.
- 특정 번들 파일의 함수가 상위에 뜨는지
JSON.parse,Array.map중첩, 정렬, diff, 큰 정규식 등이 반복되는지- 프레임워크 내부(React reconciliation 등)가 과도한지
3) 흔한 패턴: 이벤트 핸들러는 짧은데 클릭이 늦게 먹는 경우
이 경우 범인은 보통 클릭 직전에 실행되는 다른 작업입니다.
- 애니메이션 프레임에서 무거운 계산
- 스크롤 이벤트에서 동기 작업
- 타이머로 돌리는 폴링
- 큰 상태 업데이트로 인한 리렌더 폭발
타임라인에서 입력 이벤트가 발생한 시점과, 실제 핸들러가 실행되는 시점 사이의 빈틈(대기)을 확인하세요.
Long Task의 대표 원인 7가지와 확인 방법
1) 과도한 리렌더링(React/Vue 등)
증상
- 클릭 한 번에 컴포넌트 트리가 크게 갱신
- 메인 스레드에 스크립팅 시간이 길게 찍힘
대응
- 메모이제이션(
useMemo,useCallback,memo)을 “측정 후” 적용 - 리스트 렌더링 가상화
- 상태 범위를 줄이고, 큰 객체를 매번 새로 만들지 않기
2) 큰 배열 연산(정렬, 그룹핑, diff)
증상
Array.sort가 눈에 띄게 상위- 검색 입력 시마다 전체 데이터 재계산
대응
- 입력 이벤트는 디바운스
- Web Worker로 오프로드
- 증분 계산, 캐시
3) 레이아웃 스래싱(layout thrashing)
증상
Recalculate Style와Layout이 반복- DOM 측정(
getBoundingClientRect)과 DOM 변경이 번갈아 수행
대응
- 읽기와 쓰기 분리
requestAnimationFrame으로 배치
4) 무거운 서드파티 스크립트
증상
- 광고/태그 매니저/AB 테스트 SDK가 Main을 점유
대응
- 로딩 지연(
defer, 동적 import) - 필요 페이지에서만 로드
- 샌드박싱(가능하면 iframe)
5) 큰 JSON 파싱 및 직렬화
증상
JSON.parse가 Long Task 상위
대응
- 응답을 쪼개기, 필요한 필드만 받기
- 스트리밍 파서 고려
- Worker에서 파싱
6) 입력 이벤트를 과하게 처리
증상
input이벤트마다 API 호출, 무거운 필터링
대응
- 디바운스 및 취소(AbortController)
- IME 구간(
compositionstart,compositionend) 처리
7) 폴링/타이머 기반 작업이 사용자 입력과 충돌
증상
setInterval이 주기적으로 Long Task 생성
대응
- 백그라운드 탭에서는 중지
requestIdleCallback(지원/폴백 고려) 사용
실전 코드: Long Task를 “로그로 수집”해서 원인 후보 좁히기
DevTools는 강력하지만, 필드에서만 발생하는 INP 악화(특정 기기, 특정 데이터 크기)는 DevTools만으로 놓칠 수 있습니다. 이때 Long Task를 런타임에서 감지해 로그로 남기면, “어느 화면에서, 어떤 시점에, 어떤 함수가”를 빠르게 좁힐 수 있습니다.
1) Long Task 감지(PerformanceObserver)
아래 코드는 longtask 엔트리를 관찰해 duration이 큰 작업을 수집합니다.
// longtask-observer.js
export function installLongTaskObserver({ report }) {
if (typeof window === 'undefined') return;
if (!('PerformanceObserver' in window)) return;
try {
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.duration: ms
// entry.startTime: navigationStart 기준 ms
report({
name: entry.name,
duration: entry.duration,
startTime: entry.startTime,
// attribution은 브라우저별 지원이 다릅니다.
attribution: entry.attribution,
});
}
});
po.observe({ type: 'longtask', buffered: true });
return () => po.disconnect();
} catch {
// 일부 환경에서 type observe가 실패할 수 있음
}
}
report 함수에서 Sentry breadcrumb, 자체 로그 수집 API 등으로 보내면 됩니다. (개인정보 및 샘플링 정책은 반드시 적용하세요.)
2) INP도 함께 수집해 상관관계 만들기
web-vitals를 쓰면 INP를 손쉽게 수집할 수 있습니다.
// vitals.js
import { onINP } from 'web-vitals';
export function installINPReporter({ report }) {
onINP((metric) => {
report({
name: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id,
navigationType: metric.navigationType,
});
});
}
필드에서 “INP가 나쁜 세션”과 “Long Task가 많은 세션”이 겹치는지 보면, 원인이 네트워크가 아니라 메인 스레드임을 빠르게 확정할 수 있습니다.
TBT를 Lighthouse로 확인하고, INP와 연결해서 해석하기
Lighthouse의 TBT는 로딩 구간 품질을 보여주지만, 다음 이유로 INP 문제에도 힌트를 줍니다.
- 로딩 중 Long Task가 많으면, 초기 상호작용도 밀린다
- 초기 상호작용이 느린 사용자는 “사이트가 느리다”로 인지한다
Lighthouse에서 확인할 것
Diagnostics섹션의Reduce JavaScript execution timeMain-thread workbreakdownThird-party code비용
단, “TBT가 낮은데 INP가 나쁘다”면 로딩 이후에 발생하는 상호작용(예: SPA 라우팅 후 특정 위젯 조작)에서 Long Task가 터지는 케이스일 수 있습니다. 이때는 Performance 트레이스를 해당 상호작용 시점에 맞춰 떠야 합니다.
개선 전략: INP를 올리는 우선순위 체크리스트
1) 메인 스레드에서 큰 일을 빼기
- Web Worker로 계산/파싱 이동
- 큰 데이터는 서버에서 정렬/필터링 후 전달
2) 상호작용 경로를 짧게 만들기
- 클릭 핸들러에서 동기 작업 최소화
- 상태 업데이트를 쪼개고, 즉시 필요한 UI만 먼저 반영
- 비필수 작업은
requestAnimationFrame또는 유휴 시간으로 미룸
3) 렌더링 비용 줄이기
- 리스트 가상화
- 컴포넌트 분할 및 메모이제이션
- 스타일 계산을 단순화(복잡한 셀렉터, 과도한 그림자/필터 주의)
4) 서드파티 스크립트 통제
- 필요한 페이지에서만 로드
- 동적 import로 늦추기
- 태그 수를 줄이고, 비용이 큰 태그는 대체재 검토
“급락”의 진짜 원인: 배포 변경과 데이터 크기 변화
INP가 갑자기 떨어졌다면 흔히 다음 중 하나입니다.
- 특정 릴리즈에서 새 위젯/SDK가 추가됨
- 특정 이벤트 핸들러에 로깅/암호화/검증 코드가 들어감
- 데이터가 커짐(리스트 항목 수 증가, 응답 payload 증가)
- 폴링/타이머가 늘어남
여기서 중요한 건 “코드가 바뀌지 않았는데도 느려졌다”는 착시입니다. 데이터 크기나 사용자 행동 패턴이 바뀌면 같은 코드도 Long Task를 만들 수 있습니다. 필드 로그에 데이터 크기(아이템 수, payload 바이트) 같은 컨텍스트를 같이 보내면 원인 규명이 쉬워집니다.
정리: INP 급락을 Long Task로 환원해 추적하라
- INP는 상호작용 이후 다음 페인트까지의 지연이며, 메인 스레드가 막히면 악화됩니다.
- DevTools
Performance로 문제 상호작용을 녹화하고, Main 스레드의 Long Task를Bottom-Up으로 파고들면 원인 함수가 나옵니다. - Lighthouse의 TBT는 로딩 구간에서의 메인 스레드 과점유를 보여주며, 초기 INP 문제와 자주 연결됩니다.
- 필드에서만 재현되는 문제는
PerformanceObserver로 Long Task를 수집하고,web-vitals로 INP와 상관관계를 만들어 좁히는 것이 가장 빠릅니다.
다음 단계로는 “가장 큰 Long Task 1개를 없애는 PR”을 먼저 만들고, 같은 시나리오로 재측정해 INP 개선을 확인하세요. 성능 개선은 작은 승리를 반복하는 쪽이 성공 확률이 높습니다.