- Published on
Chrome INP 급락 원인 - Long Task 3분 추적기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙 중인 서비스에서 Chrome INP(Interaction to Next Paint) 가 갑자기 급락(악화)하면, 체감으로는 “클릭이 먹히다 말고 멈춘다”, “스크롤 중 버튼이 늦게 반응한다” 같은 증상이 먼저 옵니다. 문제는 이런 현상이 재현이 들쭉날쭉하고, A/B나 배포 직후가 아니면 원인 범위가 넓어 보인다는 점입니다.
하지만 실무에서 INP 급락의 1순위는 단순합니다. 메인 스레드가 50ms 이상 막히는 Long Task가 사용자 입력 직후(또는 직전)에 끼어든 것입니다. 이 글은 “원인을 찾는 데 3분”을 목표로, DevTools로 Long Task를 특정하고, 코드 레벨에서 바로 고칠 수 있는 형태로 쪼개는 과정을 체크리스트로 제공합니다.
> Long Task를 쪼개는 기법 자체는 별도 글에서 더 깊게 다뤘습니다. 필요하면 함께 보세요: Chrome INP 느림 - Long Task 분해와 scheduler 실전
INP 급락의 전형적인 패턴
INP는 “사용자 상호작용(클릭/탭/키입력 등)”이 발생한 뒤, 다음 페인트가 화면에 반영되기까지 걸린 시간을 봅니다. 즉, 다음 중 하나만 터져도 INP가 나빠집니다.
- 입력 이벤트 핸들러가 무거움(
click,pointerdown,keydown등) - 이벤트는 가볍지만, 직후에 렌더링/레이아웃/스타일 계산이 폭발
- 이벤트 직전/직후에 Long Task가 끼어 메인 스레드를 선점
- 입력 처리 자체는 빠르지만, 이후 동기 XHR / 무거운 JSON 파싱 / 대량 DOM 업데이트로 페인트가 밀림
여기서 핵심은 “입력 이벤트 핸들러 코드만 보지 말고, 입력 주변 타임라인에서 메인 스레드를 누가 잡아먹는지”를 찾는 것입니다.
3분 추적 플로우(DevTools)
아래는 실제로 ‘3분 안에’ 범인을 특정하기 위한 최소 동선입니다.
1) 재현 조건을 고정한다(30초)
- 시크릿 모드 또는 확장 프로그램 최소화
- 네트워크/CPU 조건 고정
- DevTools → Performance → 톱니바퀴 → CPU 4x slowdown(필요시)
- Network → Fast 3G/Slow 4G 등
- “사용자 인터랙션 1개”로 재현되게 시나리오를 단순화
- 예: 목록에서 필터 버튼 클릭 → 결과 렌더
재현이 흔들리면, 원인도 흔들립니다. 이 단계에서 상호작용을 1개로 고정하는 게 가장 중요합니다.
2) Performance 패널에서 INP 후보 구간을 잡는다(60초)
- DevTools → Performance
- Record(●) → 문제 인터랙션 1회 수행 → Stop
- 타임라인에서 다음을 확인
- Main 트랙에 긴 막대(노란색 Scripting, 보라색 Rendering)가 길게 이어지는 구간
- 이벤트(Interaction/Pointer/Click) 전후로 50ms+ Task가 있는지
팁:
- 상단 Summary/Bottom-Up/Call Tree에서 Scripting 시간이 큰 함수를 먼저 봅니다.
- “입력 후 화면이 늦게 바뀐다”면, Scripting뿐 아니라 Recalculate Style / Layout / Paint가 커졌는지도 같이 봅니다.
3) Long Task의 ‘소유자’를 특정한다(60~90초)
Long Task 막대를 클릭하면 하단에 Call Tree / Bottom-Up이 나옵니다. 여기서 목표는 단 하나입니다.
- “어떤 함수/번들/라이브러리가 메인 스레드를 오래 점유했는가?”
실무에서 자주 나오는 소유자 유형:
- 대량 데이터 처리:
map/filter/reduce중첩, deep clone, 정렬 - JSON 파싱/문자열 처리:
JSON.parse대용량, 정규식 폭발 - DOM 대량 조작: 루프에서
appendChild,innerHTML연속 - 레이아웃 스래싱: 읽기(
offsetHeight)와 쓰기(style)가 교차 - 프레임워크 렌더 폭발: state 업데이트 연쇄, 불필요한 re-render
여기까지 하면 “INP가 왜 나빠졌는지”가 아니라, **“누가 메인 스레드를 잡았는지”**가 나옵니다. 이게 3분 추적의 핵심 산출물입니다.
원인별로 바로 찍어보는 ‘현장 진단’ 코드
DevTools만으로도 충분하지만, 재현이 어려운 환경(특정 사용자, 특정 데이터)에서는 런타임 계측이 빠릅니다.
Long Task API로 50ms+ 작업 로그 남기기
PerformanceObserver로 Long Task를 수집할 수 있습니다(지원 브라우저 확인 필요).
// longtask-observer.js
export function observeLongTasks({ threshold = 50 } = {}) {
if (!('PerformanceObserver' in window)) return;
try {
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration < threshold) continue;
// entry.attribution은 일부 브라우저에서만 제공될 수 있음
console.log('[LongTask]', {
startTime: entry.startTime.toFixed(1),
duration: entry.duration.toFixed(1),
name: entry.name,
attribution: entry.attribution,
});
}
});
po.observe({ type: 'longtask', buffered: true });
} catch (e) {
// type 미지원 등
}
}
이 로그가 “어떤 화면/어떤 사용자 데이터에서 Long Task가 터지는지”를 좁히는 데 도움이 됩니다.
상호작용 핸들러 주변에 마커 심기
Performance 패널에서 구간을 빠르게 찾기 위해 performance.mark()를 박아두면 좋습니다.
button.addEventListener('click', async () => {
performance.mark('filter_click_start');
// 문제 구간(예: 데이터 필터링 + 렌더)
applyFilter();
renderList();
performance.mark('filter_click_end');
performance.measure('filter_click_total', 'filter_click_start', 'filter_click_end');
const [m] = performance.getEntriesByName('filter_click_total').slice(-1);
console.log('filter_click_total(ms)=', m.duration.toFixed(1));
});
이렇게 하면 DevTools 타임라인에 사용자 정의 마커가 찍혀서, Long Task와의 상관관계를 1초 만에 확인할 수 있습니다.
Long Task가 확인되면: “3가지”로 쪼개서 해결한다
원인이 확인되면 해결은 보통 아래 3가지 중 하나(또는 조합)입니다.
1) 작업을 쪼개서 프레임을 양보한다(yield)
가장 흔한 케이스: 한 번에 200ms~1s짜리 루프를 도는 경우.
function yieldToMain() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
export async function processInChunks(items, chunkSize = 500) {
const out = [];
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
// 무거운 계산
for (const x of chunk) out.push(expensiveTransform(x));
// 메인 스레드에 잠깐 양보
await yieldToMain();
}
return out;
}
이 방식은 “총 처리 시간”이 약간 늘 수 있지만, 입력 반응성(INP) 을 살리는 데 매우 효과적입니다.
2) DOM 업데이트를 배치 처리한다
DOM을 루프에서 계속 건드리면 렌더링 비용이 급증합니다. DocumentFragment로 모아서 한 번에 붙입니다.
function renderList(items) {
const ul = document.querySelector('#list');
const frag = document.createDocumentFragment();
for (const item of items) {
const li = document.createElement('li');
li.textContent = item.title;
frag.appendChild(li);
}
ul.replaceChildren(frag);
}
추가로, 레이아웃 스래싱을 피하려면 “읽기 → 쓰기” 순서를 분리하세요.
3) 메인 스레드 밖으로 옮긴다(Web Worker)
정렬/파싱/압축/대량 계산 등은 Worker로 넘기는 게 정석입니다.
// worker.js
self.onmessage = (e) => {
const { items } = e.data;
// 예: 무거운 정렬
items.sort((a, b) => b.score - a.score);
self.postMessage({ items });
};
// main.js
const worker = new Worker('/worker.js');
export function sortInWorker(items) {
return new Promise((resolve) => {
worker.onmessage = (e) => resolve(e.data.items);
worker.postMessage({ items });
});
}
Worker는 만능은 아니지만, INP 급락의 주범이 “대량 계산”이라면 가장 확실한 해결책입니다.
‘왜 갑자기’ INP가 떨어졌나: 배포 없이도 생기는 원인
INP 급락이 배포와 무관하게 발생하는 경우도 흔합니다.
데이터가 커져서 임계점을 넘었다
- 목록 아이템 수 증가
- 한 아이템의 payload가 커짐(문자열/HTML/이미지 메타)
- 특정 사용자만 가진 “특대 데이터”
이 경우는 DevTools에서 같은 코드인데도 Task duration이 데이터 크기에 비례해 증가합니다. 해결은 chunking/worker/가상 스크롤 등이 됩니다.
3rd-party 스크립트가 입력 근처를 막는다
- A/B, Analytics, Tag Manager, 광고 스크립트
- 입력 이벤트에 전역 리스너를 붙이거나, 동기 작업을 수행
진단 팁:
- Performance Call Tree에서 번들 이름/도메인을 확인
- 가능하면 해당 스크립트를 interaction 이후 지연 로딩 또는 idle 시점 로딩
GC(가비지 컬렉션) 압박
- 큰 배열/객체를 반복 생성
- 불필요한 deep copy
Heap이 요동치면 특정 타이밍에 GC가 길게 걸려 Long Task처럼 보일 수 있습니다. Memory 패널과 함께 보되, 우선은 “큰 할당을 줄이는 방향”이 빠릅니다.
재발 방지: INP 회귀를 막는 운영 체크리스트
한 번 잡았으면, 다시는 같은 유형으로 떨어지지 않게 해야 합니다.
- 성능 예산(performance budget) 설정
- 특정 상호작용의
measure()결과가 100ms/200ms를 넘으면 경고
- 특정 상호작용의
- Long Task 로깅을 샘플링으로 운영에 넣기
- 특정 페이지/특정 상호작용에서 50ms+ 빈도 증가 감지
- 릴리즈 노트에 “INP 영향 가능 변경” 태깅
- 대량 렌더, 데이터 구조 변경, 3rd-party 추가 등
문제 원인을 로그로 좁히는 방식은 DB 데드락을 로그로 역추적하는 것과 결이 같습니다. 원인이 ‘감’이 아니라 ‘증거’로 수렴해야 해결이 빨라집니다. 참고로 비슷한 접근의 트러블슈팅 글: MySQL InnoDB 데드락 로그로 원인 쿼리 찾기
마무리: 3분 안에 얻어야 하는 단 하나의 결과
INP 급락을 3분 안에 추적한다는 건, 결국 이 문장을 얻는 것입니다.
- “사용자 입력 직후, 메인 스레드를 N ms 동안 점유한 함수/스크립트는 X다.”
이 문장이 나오면 해결책은 선택지가 몇 개로 줄어듭니다.
- 쪼개서 양보(yield)
- DOM 업데이트 배치/가상화
- Worker로 오프로딩
Long Task가 확인됐는데도 어디서 쪼개야 할지 감이 안 오면, 실제 분해 패턴과 scheduler 활용을 더 자세히 정리한 글을 같이 보세요: Chrome INP 느림 - Long Task 분해와 scheduler 실전