- Published on
Chrome INP 급락 원인 찾기 - Long Task 추적
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 응답도 빠르고 LCP도 괜찮은데, 어느 날부터 Chrome에서 INP가 급락(악화)하는 경우가 있습니다. 이때 가장 흔한 범인은 메인 스레드를 오래 점유하는 Long Task입니다. 사용자가 클릭하거나 키 입력을 해도, 메인 스레드가 바쁘면 이벤트 핸들러 실행 자체가 지연되고 결과적으로 INP가 나빠집니다.
이 글은 "INP가 왜 떨어졌는지"를 감으로 추측하지 않고, 측정 → 재현 → 원인 분해 → 개선 검증까지 이어지는 흐름으로 Long Task를 추적하는 방법을 다룹니다.
INP와 Long Task의 관계를 먼저 정리
INP(Interaction to Next Paint)는 사용자의 인터랙션(클릭, 탭, 키 입력 등)에 대해 **다음 화면 업데이트(페인트)**가 일어날 때까지의 지연을 측정합니다. INP가 나빠지는 전형적인 패턴은 다음 중 하나입니다.
- 이벤트가 들어왔는데 메인 스레드가 바빠서 이벤트 핸들러 실행이 늦어짐
- 핸들러는 빨리 시작했지만, 핸들러 내부 작업이 무거워 처리가 길어짐
- 핸들러는 끝났는데, 레이아웃·스타일 계산·페인트가 밀려 다음 페인트가 늦어짐
여기서 Long Task는 보통 50ms를 초과해 메인 스레드를 점유하는 작업을 의미합니다. Long Task가 연속으로 발생하면, 사용자 입력은 큐에 쌓이고 INP는 급격히 악화됩니다.
1) “진짜 INP 문제”인지 먼저 확인하기
가끔 INP 급락으로 보이지만, 실제로는 측정/수집 파이프라인 문제일 수 있습니다. 예를 들어 RUM 샘플링 변경, 라우팅 변경, 특정 브라우저 버전에서의 계측 버그 등이 원인일 수 있죠.
최소한 아래를 먼저 확인하세요.
- INP 악화가 특정 페이지/라우트에만 국한되는지
- 특정 디바이스(저사양 안드로이드)나 특정 브라우저 버전에 집중되는지
- 배포 시점과 상관관계가 있는지
Next.js를 쓰는 경우, 클라이언트에서만 계측해야 하는데 서버에서 window를 참조하며 예외가 나면 계측이 끊기기도 합니다. 관련해서는 Next.js RSC에서 window is not defined 해결법도 함께 점검해두면 좋습니다.
2) 현장에서 쓰는 INP/Long Task RUM 계측 코드
DevTools에서 재현이 안 되거나, 특정 사용자군에서만 느려지는 경우는 RUM이 필수입니다. 핵심은 두 가지입니다.
web-vitals로 INP 값을 수집PerformanceObserver로 Long Task 엔트리를 수집
아래 예시는 Next.js/SPA 어디서든 쓸 수 있는 형태로 작성했습니다.
// metrics.ts
import { onINP, type Metric } from 'web-vitals'
type LongTaskPayload = {
name: string
startTime: number
duration: number
attribution?: unknown
}
export function initVitalsAndLongTasks(send: (event: string, payload: unknown) => void) {
// 1) INP
onINP((metric: Metric) => {
send('webvitals_inp', {
id: metric.id,
value: metric.value,
rating: metric.rating,
navigationType: metric.navigationType,
})
})
// 2) Long Task
if (typeof PerformanceObserver === 'undefined') return
try {
const obs = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entryType: 'longtask'
const payload: LongTaskPayload = {
name: entry.name,
startTime: entry.startTime,
duration: entry.duration,
}
send('perf_longtask', payload)
}
})
obs.observe({ entryTypes: ['longtask'] as any })
} catch {
// 일부 브라우저/환경에서 entryTypes 미지원 가능
}
}
수집 시 팁:
- Long Task는 이벤트 폭주가 가능하니, 샘플링하거나
duration이 큰 것만 전송하세요. - 라우트, 사용자 에이전트, 디바이스 메모리(
navigator.deviceMemory) 같은 차원을 같이 보내면 “특정 기기에서만” 문제를 빠르게 찾습니다.
3) Chrome DevTools로 Long Task를 “눈으로” 찾는 절차
RUM이 방향을 제시해주면, DevTools는 원인을 확정해줍니다.
3-1) Performance 패널: Interactions와 Main을 함께 보기
- DevTools
Performance탭에서 Record 시작 - 문제되는 상호작용(클릭/탭/키 입력)을 실제로 수행
- Record 종료
이후 아래를 집중해서 봅니다.
Interactions트랙에서 특정 인터랙션이 길게 늘어진 구간Main트랙에서 긴 노란색(스크립트) 또는 보라색(렌더링) 블록Long task로 표시되는 작업(환경에 따라 마커가 다를 수 있음)
핵심은 “인터랙션이 시작된 시점”과 “메인 스레드가 막힌 구간”을 같은 타임라인에서 겹쳐 보는 것입니다.
3-2) Call Tree / Bottom-Up으로 “무거운 함수”를 특정
Main에서 길게 늘어진 Task를 클릭한 다음:
Call Tree: 호출 흐름 관점에서 어떤 함수가 시간을 먹었는지Bottom-Up: 누적 비용이 큰 함수부터 역으로 정렬
여기서 자주 나오는 패턴:
- 대량 JSON 파싱/정렬/필터링
- 큰 DOM 업데이트 후 강제 동기 레이아웃(레이아웃 스래싱)
- 캔버스/차트 렌더링
- 서드파티 스크립트(태그 매니저, 광고, A/B) 초기화
3-3) “왜 지금 실행됐지?”를 찾기: Initiator와 이벤트 연결
Long Task가 보이면 “무슨 이벤트가 트리거했는가”를 찾아야 합니다.
- 클릭 핸들러에서 시작됐는지
- 라우트 전환에 묶여있는지
setTimeout/requestAnimationFrame/Promise 체인인지
여기서 중요한 건, 사용자 입력과 무관한 작업이 인터랙션 직전에 끼어들어 INP를 악화시키는 케이스가 많다는 점입니다. 예를 들어 라우트 진입 시 프리페치, 분석 SDK 초기화, 대량 컴포넌트 마운트 등이 클릭 직후 메인 스레드를 먹어버릴 수 있습니다.
4) Long Task 원인을 4가지로 분류하고 접근하기
Long Task는 원인별로 해결법이 다릅니다. 아래 4가지로 먼저 분류하면 대응이 빨라집니다.
4-1) 순수 JS 계산 과다
예: 정렬, 검색 인덱싱, 대량 데이터 변환
해결 방향:
- 작업을 쪼개서 프레임 사이에 양보
- Web Worker로 오프로드
- 캐싱/메모이제이션
작업 쪼개기 예시(프레임 양보):
function yieldToMain(): Promise<void> {
return new Promise((resolve) => {
// requestAnimationFrame으로 다음 프레임에 넘김
requestAnimationFrame(() => resolve())
})
}
export async function chunkedMap<T, R>(items: T[], fn: (t: T) => R, chunkSize = 200) {
const out: R[] = []
for (let i = 0; i < items.length; i++) {
out.push(fn(items[i]))
if (i % chunkSize === 0) {
await yieldToMain()
}
}
return out
}
주의할 점은, 단순히 setTimeout으로 미루면 입력 지연이 더 커질 수도 있습니다. 실제로는 requestAnimationFrame 또는 scheduler.postTask(지원 환경에서) 같은 방식으로 렌더링 타이밍과 조화시키는 편이 안전합니다.
4-2) 렌더링 비용 과다(레이아웃/스타일/페인트)
예: 리스트 3천 개 렌더, 이미지 그리드, 복잡한 CSS
해결 방향:
- 화면 밖 DOM은 렌더링/레이아웃에서 제외
- 컴포넌트 단위로 렌더링 격리
- 가상 스크롤 적용
CSS 레벨에서의 큰 효과는 content-visibility와 contain입니다. 실제 적용 사례와 주의점은 CSS contain·content-visibility로 렌더링 튜닝 글을 함께 보면 좋습니다.
간단 예시:
.card-list {
content-visibility: auto;
contain-intrinsic-size: 1px 800px; /* 대략적인 높이 힌트 */
}
.card {
contain: layout paint style;
}
이 조합은 “초기 렌더링 때 보이지 않는 영역”의 비용을 줄여, 인터랙션 직후 메인 스레드가 렌더링으로 막히는 상황을 완화하는 데 도움이 됩니다.
4-3) 서드파티 스크립트가 메인 스레드를 점유
예: 태그 매니저가 DOM을 스캔, 광고 SDK가 레이아웃을 반복 측정
해결 방향:
- 로딩 시점을 늦추기(사용자 첫 인터랙션 이후)
- 필요한 페이지에서만 로드
async/defer적용 및 초기화 비용 측정
첫 인터랙션 이후 로드 예시:
<script>
function loadThirdParty() {
var s = document.createElement('script')
s.src = 'https://example.com/sdk.js'
s.async = true
document.head.appendChild(s)
}
// 첫 사용자 입력 이후에만 로드
window.addEventListener('pointerdown', function onFirstInput() {
window.removeEventListener('pointerdown', onFirstInput)
loadThirdParty()
}, { once: true, passive: true })
</script>
이 전략은 INP 관점에서 특히 효과적입니다. “사용자가 뭔가 하려는 순간”에 SDK 초기화가 겹치지만 않게 하면 되기 때문입니다.
4-4) 프레임워크/상태관리로 인한 과도한 리렌더
예: 상태 변경 한 번에 큰 트리 전체가 리렌더, 메모이제이션 누락
해결 방향:
- 리렌더 범위를 줄이기(컴포넌트 분리)
- 비싼 계산은
useMemo/selector 메모이제이션 - 이벤트 핸들러에서 상태 변경을 배치/디바운스
React라면 Profiler로 “어떤 컴포넌트가 오래 걸렸는지”를 확인하고, 그 컴포넌트가 생성하는 DOM/스타일 비용까지 이어서 Performance 패널에서 확인하는 식으로 교차 검증합니다.
5) “INP 급락”을 만드는 흔한 함정 6가지
실무에서 자주 만난 패턴을 체크리스트로 정리합니다.
- 클릭 시 동기적으로 큰 데이터를
JSON.parse후 바로 렌더 - 입력 이벤트(
input,keydown)에서 매 타이핑마다 무거운 필터링 수행 - 스크롤/리사이즈 핸들러가
passive가 아니거나, 내부에서 레이아웃을 강제 - DOM 측정(
getBoundingClientRect)과 DOM 변경을 섞어 레이아웃 스래싱 유발 - 라우트 전환 시 대규모 컴포넌트 마운트가 첫 인터랙션과 겹침
- 서드파티 SDK가 초기화 중 DOM 전체를 순회
이 중 4번은 특히 전형적입니다. 다음처럼 “읽기-쓰기-읽기”가 섞이면 레이아웃이 반복 계산됩니다.
// 나쁜 예: 측정과 변경이 섞여 레이아웃이 흔들릴 수 있음
const h1 = el.getBoundingClientRect().height
el.style.height = (h1 + 10) + 'px'
const h2 = el.getBoundingClientRect().height
가능하면 측정은 모아서 하고, 변경도 모아서 하세요.
6) 개선 후 검증: DevTools만 믿지 말고 “분포”를 보자
INP는 단일 값이 아니라 사용자 환경에 따라 분포가 크게 벌어집니다. 개선 검증은 아래 순서가 안전합니다.
- 로컬에서 Performance 레코딩으로 Long Task가 줄었는지 확인
- 실기기(특히 저사양 안드로이드)에서 재현 테스트
- RUM에서 p75, p90 같은 분위수로 개선 확인
특히 INP는 “가끔 터지는 매우 큰 지연”이 p75를 끌어올리는 경우가 많아서, 평균이 아니라 분위수를 봐야 합니다.
7) 결론: Long Task는 “찾는 기술”이 절반이다
INP가 급락했을 때 중요한 건, 최적화 기법을 많이 아는 것보다 Long Task를 정확히 찾아내는 관측 능력입니다.
- RUM으로 어디서 터지는지 좁히고
- DevTools Performance에서 인터랙션과 Main 스레드 블로킹을 겹쳐 보고
- 원인을 JS 계산, 렌더링, 서드파티, 리렌더로 분류한 뒤
- 쪼개기/오프로드/렌더링 격리/로딩 지연 같은 처방을 적용
이 흐름을 팀의 표준 디버깅 런북으로 만들어두면, 다음 INP 이슈는 훨씬 빠르게 진단할 수 있습니다.