- Published on
Chrome INP 점수 급락 - Long Task 5분 추적법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
INP(Interaction to Next Paint)는 “사용자가 클릭/탭/키 입력을 했을 때 다음 페인트가 일어날 때까지”의 지연을 측정합니다. 최근 Chrome에서 Core Web Vitals로 중요도가 올라가면서, 어느 날 갑자기 INP 점수가 급락(예: 150ms → 600ms+)하는 사례가 흔해졌습니다. 이때 가장 흔한 범인은 메인 스레드(Main Thread)를 50ms 이상 점유하는 Long Task입니다.
문제는 “INP가 나빠졌다”는 결과만으로는 어디가 느린지 감이 안 잡힌다는 점입니다. 이 글은 Chrome DevTools만으로 5분 안에 Long Task를 찾아내고, 그것이 실제 INP 악화로 이어지는 경로(이벤트 처리 → 렌더링/페인트)를 확인한 뒤, 바로 적용 가능한 개선 패턴까지 연결하는 것을 목표로 합니다.
> 장애/성능 이슈를 빠르게 좁혀가는 접근은 인프라에서도 동일합니다. 크래시 원인을 10분 내 좁히는 방식이 궁금하다면 K8s CrashLoopBackOff 원인 10분내 찾는 법도 함께 참고하면 “가설→증거→확정” 흐름이 잡힙니다.
1) INP 급락의 전형적인 패턴: Long Task + 입력 큐 지연
INP는 크게 아래 구간의 합으로 악화됩니다.
- Input delay: 사용자의 입력 이벤트가 발생했지만 메인 스레드가 바빠서 이벤트 핸들러가 늦게 실행됨
- Processing time: 이벤트 핸들러 자체가 오래 걸림(동기 JS, 레이아웃 스래싱 등)
- Presentation delay: 처리 후 다음 페인트까지 지연(스타일/레이아웃/페인트/컴포지팅, 또는 프레임 드랍)
Long Task가 있으면 (1)과 (2)가 동시에 커지고, 결과적으로 INP가 급락합니다. 특히 아래 상황은 “갑자기” 나빠지기 쉽습니다.
- 특정 배포에서 번들 크기 증가 → 초기/상호작용 시 파싱/실행이 길어짐
- 사용자 행동(검색, 필터, 드래그)에서 큰 JSON 처리/정렬/렌더링이 동기 실행됨
- 3rd-party 스크립트(태그, A/B, 위젯)가 메인 스레드를 잠식
- React/Vue 등에서 리렌더 범위가 커짐(메모이제이션 붕괴)
2) 5분 추적 로드맵(DevTools만으로)
아래 순서대로 하면, “INP가 나쁨”을 “어떤 함수가 메인 스레드를 몇 ms 점유”로 바꿀 수 있습니다.
- 재현 조건 고정: 느린 상호작용 1개를 정함(예: 필터 클릭, 장바구니 버튼)
- Performance 패널 기록: 상호작용 직전부터 3~5초만 기록
- Long Task 찾기: Main 트랙에서 50ms+ 태스크 확인
- 원인 함수 스택 확인: Bottom-Up / Call Tree로 “시간을 먹는 함수” 특정
- 개선 전/후 비교: 동일 시나리오로 다시 기록해 Long Task 제거/축소 확인
3) DevTools Performance로 Long Task를 1분 안에 찾기
3.1 기록 방법(가장 빠른 세팅)
- Chrome에서 문제 페이지 열기
- DevTools → Performance
- 우측 상단 톱니바퀴(설정)에서 필요 시:
- CPU Throttling: 4x(로컬에서 재현이 안 되면)
- Network Throttling: Fast 3G 정도(초기 로딩 영향이 크다면)
- Record 누르고, 바로 문제 상호작용 1회 수행
- 3~5초 내로 Stop
기록을 길게 하면 분석 시간이 늘고, “핵심 상호작용”이 묻힙니다. INP는 상호작용 중심이므로 짧게 끊는 게 유리합니다.
3.2 Long Task는 어디에 보이나?
- Performance 타임라인에서 Main 트랙을 봅니다.
- 노란색(스크립트) 구간이 길게 이어지고, Task가 50ms 이상이면 Long Task 가능성이 큽니다.
- 확대(드래그)해서 문제 상호작용 시점 주변만 좁힙니다.
팁:
- 상호작용 직후에 긴 노란 블록이 있으면 이벤트 핸들러/동기 작업 가능성
- 상호작용 이전부터 계속 긴 작업이 있으면 입력 큐 지연(Input delay) 가능성
4) “어떤 함수가 느린가”를 2분 안에 특정하기
4.1 Main 트랙의 Task 클릭 → Summary 확인
긴 Task를 클릭하면 하단 Summary에 다음이 보입니다.
- Task duration
- Scripting / Rendering / Painting 비중
- 관련된 이벤트(Click, Pointerdown 등)
여기서 Scripting 비중이 크면 JS 실행이 문제고, Rendering 비중이 크면 레이아웃/스타일 계산이 문제일 확률이 큽니다.
4.2 Bottom-Up / Call Tree로 “시간 먹는 함수” 찾기
DevTools 하단 탭에서:
- Bottom-Up: “가장 시간이 많이 든 함수”부터 역으로 보여줌(초기 분석에 최적)
- Call Tree: 호출 흐름을 따라가며 원인 파악
실전 팁:
- Self Time이 큰 함수: 그 함수 자체가 CPU를 많이 씀(파싱, 정렬, 암호화, 큰 루프)
- Total Time이 큰 함수: 하위 호출 포함해서 전체 비용이 큼(렌더 트리거, 프레임워크 업데이트)
여기서 함수/파일/라인이 특정되면, 5분 추적의 80%는 끝난 겁니다.
5) Long Task 유형별 처방전(바로 적용 가능한 패턴)
이제부터는 “원인 함수”가 무엇이냐에 따라 해결책이 갈립니다. 아래는 INP 급락에서 자주 나오는 유형과 대응입니다.
5.1 큰 배열 정렬/필터/집계를 클릭 핸들러에서 동기 실행
문제 패턴:
- 클릭 이벤트에서
Array.sort, 복잡한reduce, 대량 데이터 가공을 한 번에 처리 - 처리 후 setState/DOM 업데이트까지 이어져 한 방에 Long Task
해결 패턴:
- 작업을 쪼개서 메인 스레드를 양보(yield)하기
// 긴 작업을 작은 청크로 나누고, 프레임 사이에 양보
async function chunkedProcess(items, chunkSize = 500) {
const result = [];
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
// CPU 작업(예시)
for (const x of chunk) result.push(expensiveTransform(x));
// 브라우저에 제어권 양보
await new Promise(requestAnimationFrame);
}
return result;
}
button.addEventListener('click', async () => {
// 즉시 UI 피드백(로딩 상태)부터
setLoading(true);
const processed = await chunkedProcess(bigList);
setData(processed);
setLoading(false);
});
- 정말 CPU 바운드면 Web Worker로 오프로드
// main.js
const worker = new Worker('/worker.js');
function runInWorker(payload) {
return new Promise((resolve, reject) => {
worker.onmessage = (e) => resolve(e.data);
worker.onerror = reject;
worker.postMessage(payload);
});
}
button.addEventListener('click', async () => {
setLoading(true);
const processed = await runInWorker({ items: bigList });
setData(processed);
setLoading(false);
});
// worker.js
self.onmessage = (e) => {
const { items } = e.data;
const processed = items
.map(expensiveTransform)
.sort((a, b) => a.key - b.key);
self.postMessage(processed);
};
핵심은 “입력 이벤트 직후”에 메인 스레드가 오래 잡히지 않게 만드는 것입니다.
5.2 레이아웃 스래싱(Layout Thrashing): 읽기/쓰기 교차
문제 패턴:
- 루프에서
getBoundingClientRect()(읽기)와style.width = ...(쓰기)를 번갈아 수행 - 매번 강제 동기 레이아웃이 발생해 Rendering 시간이 폭증
해결: 읽기와 쓰기를 분리(batch)합니다.
// BAD: read/write가 섞여 강제 레이아웃 반복
for (const el of items) {
const w = el.getBoundingClientRect().width;
el.style.width = (w + 10) + 'px';
}
// GOOD: 먼저 읽기만, 그 다음 쓰기만
const widths = items.map(el => el.getBoundingClientRect().width);
items.forEach((el, i) => {
el.style.width = (widths[i] + 10) + 'px';
});
추가로, 애니메이션/이동은 top/left보다 transform을 우선 고려하면 페인트 비용이 줄어 INP에도 간접적으로 유리합니다.
5.3 3rd-party 스크립트가 메인 스레드를 점유
문제 패턴:
- 태그 매니저/광고/위젯이 상호작용 직전/직후에 긴 스크립트를 실행
- 내 코드가 아니라서 수정이 어려움
대응:
- 로딩 시점 지연:
defer,async, 또는 사용자 동의/첫 상호작용 이후 로드 - 기능 분리: 꼭 필요한 페이지에서만 로드
- DevTools에서 해당 스크립트 URL/도메인을 확인 후 차단 실험(확인용)
<!-- 예: 초기 렌더에 중요하지 않다면 defer 고려 -->
<script src="https://example.com/widget.js" defer></script>
또는 “첫 상호작용 이후” 로드:
let loaded = false;
function load3p() {
if (loaded) return;
loaded = true;
const s = document.createElement('script');
s.src = 'https://example.com/widget.js';
s.async = true;
document.head.appendChild(s);
}
window.addEventListener('pointerdown', load3p, { once: true, passive: true });
6) INP 개선을 ‘측정 가능’하게 만드는 최소 계측 코드
DevTools로 원인을 찾았더라도, 배포 후 실제 사용자의 INP(RUM)가 개선되는지 확인해야 합니다. 가장 쉬운 방법은 web-vitals 라이브러리로 INP를 수집해 로그/분석 도구로 보내는 것입니다.
import { onINP } from 'web-vitals';
onINP((metric) => {
// metric.value: INP(ms)
// metric.attribution: 어떤 상호작용이 문제였는지 힌트
console.log('INP', metric.value, metric);
// 예: 수집 엔드포인트로 전송
navigator.sendBeacon('/vitals', JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
rating: metric.rating,
attribution: metric.attribution,
}));
});
이렇게 하면 “특정 릴리즈 이후 특정 페이지에서 INP가 악화” 같은 회귀를 빠르게 잡을 수 있습니다. 재시도/백오프 같은 전송 안정화는 트래픽이 커질수록 중요해지는데, 설계 감각은 OpenAI 429·insufficient_quota 재시도와 백오프 설계에서 다룬 패턴과 유사합니다.
7) 5분 디버깅 체크리스트(실전용)
아래 체크리스트대로 움직이면 “INP 급락”을 빠르게 수습할 수 있습니다.
- 느린 상호작용 1개만 고정(버튼 클릭/검색 입력 등)
- Performance 기록은 짧게(3~5초)
- Main 트랙에서 50ms+ 작업을 먼저 클릭
- Summary에서 Scripting vs Rendering 비중 확인
- Bottom-Up에서 Self Time 큰 함수 1~3개를 뽑기
- 아래 중 무엇인지 분류
- 큰 동기 JS(정렬/가공/파싱)
- 레이아웃 스래싱
- 3rd-party 스크립트
- 프레임워크 리렌더 폭증
- 개선 적용 후 동일 시나리오로 재기록해 Long Task가 쪼개졌는지/사라졌는지 확인
결론
Chrome INP 점수 급락은 대부분 “사용자 입력 타이밍에 메인 스레드를 오래 잡는 Long Task”로 귀결됩니다. DevTools Performance에서 Main 트랙의 긴 Task를 찾아 Bottom-Up으로 원인 함수를 특정하면, 5분 안에 문제 지점을 코드 레벨로 좁힐 수 있습니다. 이후에는 작업 쪼개기(yield), Web Worker 오프로드, 레이아웃 스래싱 제거, 3rd-party 로딩 지연 같은 정석 처방을 적용하고, web-vitals로 실제 사용자 INP 개선까지 확인하는 흐름을 만들면 회귀에도 강해집니다.
다음 단계로는 “어떤 상호작용이 INP를 대표(worst interaction)하는지”를 RUM으로 모으고, 상위 1~2개 상호작용만 집중적으로 최적화하는 전략이 가장 비용 대비 효과가 좋습니다.