Published on

Chrome INP 점수 급락 - Long Task 5분 추적법

Authors

서론

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는 크게 아래 구간의 합으로 악화됩니다.

  1. Input delay: 사용자의 입력 이벤트가 발생했지만 메인 스레드가 바빠서 이벤트 핸들러가 늦게 실행됨
  2. Processing time: 이벤트 핸들러 자체가 오래 걸림(동기 JS, 레이아웃 스래싱 등)
  3. Presentation delay: 처리 후 다음 페인트까지 지연(스타일/레이아웃/페인트/컴포지팅, 또는 프레임 드랍)

Long Task가 있으면 (1)과 (2)가 동시에 커지고, 결과적으로 INP가 급락합니다. 특히 아래 상황은 “갑자기” 나빠지기 쉽습니다.

  • 특정 배포에서 번들 크기 증가 → 초기/상호작용 시 파싱/실행이 길어짐
  • 사용자 행동(검색, 필터, 드래그)에서 큰 JSON 처리/정렬/렌더링이 동기 실행됨
  • 3rd-party 스크립트(태그, A/B, 위젯)가 메인 스레드를 잠식
  • React/Vue 등에서 리렌더 범위가 커짐(메모이제이션 붕괴)

2) 5분 추적 로드맵(DevTools만으로)

아래 순서대로 하면, “INP가 나쁨”을 “어떤 함수가 메인 스레드를 몇 ms 점유”로 바꿀 수 있습니다.

  1. 재현 조건 고정: 느린 상호작용 1개를 정함(예: 필터 클릭, 장바구니 버튼)
  2. Performance 패널 기록: 상호작용 직전부터 3~5초만 기록
  3. Long Task 찾기: Main 트랙에서 50ms+ 태스크 확인
  4. 원인 함수 스택 확인: Bottom-Up / Call Tree로 “시간을 먹는 함수” 특정
  5. 개선 전/후 비교: 동일 시나리오로 다시 기록해 Long Task 제거/축소 확인

3) DevTools Performance로 Long Task를 1분 안에 찾기

3.1 기록 방법(가장 빠른 세팅)

  1. Chrome에서 문제 페이지 열기
  2. DevTools → Performance
  3. 우측 상단 톱니바퀴(설정)에서 필요 시:
    • CPU Throttling: 4x(로컬에서 재현이 안 되면)
    • Network Throttling: Fast 3G 정도(초기 로딩 영향이 크다면)
  4. Record 누르고, 바로 문제 상호작용 1회 수행
  5. 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

해결 패턴:

  1. 작업을 쪼개서 메인 스레드를 양보(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);
});
  1. 정말 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. 느린 상호작용 1개만 고정(버튼 클릭/검색 입력 등)
  2. Performance 기록은 짧게(3~5초)
  3. Main 트랙에서 50ms+ 작업을 먼저 클릭
  4. Summary에서 Scripting vs Rendering 비중 확인
  5. Bottom-Up에서 Self Time 큰 함수 1~3개를 뽑기
  6. 아래 중 무엇인지 분류
    • 큰 동기 JS(정렬/가공/파싱)
    • 레이아웃 스래싱
    • 3rd-party 스크립트
    • 프레임워크 리렌더 폭증
  7. 개선 적용 후 동일 시나리오로 재기록해 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개 상호작용만 집중적으로 최적화하는 전략이 가장 비용 대비 효과가 좋습니다.