Published on

Chrome INP 급락 원인 찾기 - Long Task 추적

Authors

서빙 지표가 멀쩡한데도 사용자 체감이 갑자기 나빠졌다면, 이제는 LCP보다 INP(Interaction to Next Paint)를 먼저 의심해야 합니다. 특히 Chrome에서 INP가 급락(악화)하는 케이스는 대부분 메인 스레드가 바빠서 입력을 제때 처리하지 못하는 상황으로 수렴합니다. 그 중심에 있는 것이 바로 Long Task입니다.

이 글에서는 다음을 목표로 합니다.

  • INP가 왜 떨어지는지(구성 요소 관점)
  • Long Task가 INP에 어떤 방식으로 영향을 주는지
  • Chrome DevTools로 Long Task를 “재현 → 식별 → 원인 코드 pinpoint” 하는 절차
  • 앱 코드에서 INP/Long Task를 계측해 회귀를 막는 방법

INP가 급락하는 전형적인 메커니즘

INP는 사용자의 상호작용(클릭, 탭, 키 입력 등) 이후 다음 페인트가 화면에 나타날 때까지의 지연을 측정합니다. 실무에서 INP 악화는 대개 아래 중 하나(혹은 복합)로 발생합니다.

1) 입력 지연(Input delay): 이벤트를 받기까지 오래 걸림

메인 스레드가 이미 긴 작업을 수행 중이면, 사용자가 클릭해도 이벤트 루프가 이벤트를 즉시 처리하지 못합니다. 이때 지연이 발생합니다.

2) 처리 시간(Processing time): 이벤트 핸들러가 무거움

클릭 핸들러에서 큰 JSON 파싱, 복잡한 상태 계산, 대량 DOM 업데이트 등을 하면 핸들러 자체가 길어집니다.

3) 표시 지연(Presentation delay): 렌더/레이아웃/페인트가 밀림

핸들러는 짧아도, 그 결과로 레이아웃 스래싱이 생기거나 큰 페인트가 발생하면 “다음 페인트”가 늦어집니다.

이 세 구간 모두 **Long Task(50ms 이상 메인 스레드 점유)**와 강하게 연결됩니다. 즉, INP 급락의 출발점은 “메인 스레드가 50ms 이상 묶이는 구간이 늘었다”로 보는 게 가장 빠릅니다.


Long Task란 무엇이고 왜 INP를 망가뜨리나

Long Task는 브라우저가 메인 스레드에서 수행하는 작업이 50ms를 초과하는 경우를 말합니다. 메인 스레드는 대략 아래를 모두 담당합니다.

  • JS 실행
  • 스타일 계산
  • 레이아웃
  • 페인트/컴포지팅 일부
  • 입력 이벤트 처리

따라서 Long Task가 존재하면 다음이 동시에 발생할 수 있습니다.

  • 사용자의 입력 이벤트가 큐에 쌓여 대기
  • 이벤트 핸들러 실행이 늦어짐
  • 렌더링이 밀려 “Next Paint”가 늦어짐

결론적으로 Long Task는 INP의 세 구간을 모두 악화시킬 수 있는 “공통 원인”입니다.


1단계: DevTools로 INP 악화 상황 재현하기

INP는 “특정 상호작용에서 최악에 가까운 지연”을 잡아내는 지표라서, 먼저 문제 상호작용을 재현해야 합니다.

재현 체크리스트

  • 시크릿 모드(확장 프로그램 영향 최소화)
  • CPU Throttling(예: 4x 또는 6x)로 저사양 환경 시뮬레이션
  • Network Throttling은 INP 자체보다 “로딩 후 상호작용”에 영향을 주므로, 재현 목적에 맞게 선택

DevTools Performance 기록 절차

  1. Chrome DevTools Performance 탭 이동
  2. 설정에서 Screenshots 체크(상호작용 시점 확인에 유용)
  3. Record 시작
  4. 문제 상호작용(클릭/탭/입력)을 실제로 수행
  5. Stop

여기서 중요한 포인트는 “상호작용 직전부터 직후 페인트까지”가 타임라인에 포함되도록 녹화 구간을 적절히 잡는 것입니다.


2단계: Long Task를 타임라인에서 빠르게 찾는 법

Performance 결과에서 다음을 우선 확인합니다.

Main 스레드의 긴 막대(노란색 JS, 보라색 렌더링)

  • Main 트랙에서 50ms 이상 점유하는 덩어리를 찾습니다.
  • 노란색이 길면 JS 실행이 무거운 경우가 많고,
  • 보라색(렌더링/레이아웃)이 길면 DOM 변경 패턴이나 CSS/레이아웃 문제가 많습니다.

Interactions(상호작용) 마커 확인

최근 Chrome은 상호작용을 표시해주며, 해당 상호작용이 어떤 작업에 의해 지연되는지 연결해서 볼 수 있습니다. 상호작용 마커 근처의 Long Task가 유력 용의자입니다.

Bottom-up / Call tree로 “원인 함수”까지 들어가기

  • Bottom-up: 가장 시간을 많이 먹는 함수(누적) 중심
  • Call tree: 호출 흐름 중심

실전에서는 Bottom-up으로 “가장 무거운 함수”를 먼저 찾고, 그 다음 Call tree에서 “왜 그 함수가 호출됐는지”를 추적하는 방식이 빠릅니다.


3단계: Long Task의 흔한 원인 패턴 7가지

아래는 INP 급락을 만들기 쉬운 대표 패턴입니다.

1) 클릭 핸들러에서 동기적으로 큰 작업 수행

예: 대용량 데이터 정렬/필터, 큰 JSON 파싱, 암호화/해시 등

2) 상태 업데이트 폭발로 인한 대량 렌더링

React/Vue 등에서 한 번의 상호작용이 수백~수천 노드 업데이트로 이어지면 메인 스레드가 잠깁니다.

3) 레이아웃 스래싱(layout thrashing)

DOM 읽기(getBoundingClientRect)와 쓰기(style 변경)를 번갈아 수행하면 강제 동기 레이아웃이 반복됩니다.

4) 큰 페인트/컴포지팅 비용

큰 그림자, 블러, 대형 fixed 요소, 거대한 이미지 리사이즈 등

5) 서드파티 스크립트의 메인 스레드 점유

A/B 테스트, 광고, 분석 스크립트가 상호작용 직후 실행되면 INP를 망치기 쉽습니다.

6) 입력 이벤트를 과도하게 처리

input/keydown에 매 타이핑마다 무거운 검증/검색 수행

7) 긴 microtask 체인

Promise.then이 연쇄적으로 이어져 렌더링 기회를 뺏는 경우가 있습니다.


4단계: 코드로 Long Task를 “쪼개서” INP 개선하기

핵심은 “메인 스레드를 50ms 이상 점유하지 않게 쪼개기”입니다.

방법 A: 작업을 청크로 쪼개고 requestAnimationFrame/setTimeout로 양보

아래 예시는 무거운 루프를 프레임 사이로 분할합니다.

function chunkedWork(items, chunkSize = 500) {
  let i = 0;

  function runChunk() {
    const end = Math.min(i + chunkSize, items.length);
    for (; i < end; i++) {
      // 무거운 계산
      items[i].score = Math.sqrt(items[i].value) * 42;
    }

    if (i < items.length) {
      // 렌더링/입력 처리를 위해 메인 스레드에 양보
      requestAnimationFrame(runChunk);
    }
  }

  requestAnimationFrame(runChunk);
}
  • 장점: 구현이 단순하고 즉시 효과를 보는 경우가 많음
  • 단점: 총 처리 시간은 늘 수 있음(하지만 INP는 개선)

방법 B: requestIdleCallback로 유휴 시간에 처리(주의점 포함)

function idleWork(items) {
  let i = 0;

  function work(deadline) {
    while (deadline.timeRemaining() > 5 && i < items.length) {
      items[i].score = Math.sqrt(items[i].value) * 42;
      i++;
    }

    if (i < items.length) {
      requestIdleCallback(work);
    }
  }

  requestIdleCallback(work);
}

주의:

  • requestIdleCallback은 모든 환경에서 동일하게 동작하지 않을 수 있고, 백그라운드 탭에서는 실행이 크게 지연될 수 있습니다.
  • 사용자 상호작용 직후 “반드시 끝나야 하는 일”에는 부적합합니다.

방법 C: Web Worker로 메인 스레드 밖으로 이동

정렬/필터/파싱 같은 CPU 작업은 Worker가 정석입니다.

worker.js

self.onmessage = (e) => {
  const items = e.data;
  // 무거운 작업
  items.sort((a, b) => a.value - b.value);
  self.postMessage(items);
};

메인 코드

const worker = new Worker(new URL('./worker.js', import.meta.url));

worker.onmessage = (e) => {
  const sorted = e.data;
  // 결과 반영(UI 업데이트)
  render(sorted);
};

function onClickSort(items) {
  worker.postMessage(items);
}
  • 장점: 메인 스레드 Long Task를 근본적으로 제거
  • 단점: 직렬화 비용, 코드 구조 변경 필요

5단계: Long Task를 “측정”해서 회귀를 막기

재현이 어렵거나, 배포 후 특정 환경에서만 INP가 떨어진다면 런타임 계측이 필요합니다.

Long Task 관측: PerformanceObserver로 longtask 수집

export function observeLongTasks(onEntry) {
  if (!('PerformanceObserver' in window)) return;

  try {
    const po = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        // entry.duration: Long Task 지속 시간(ms)
        onEntry({
          name: entry.name,
          startTime: entry.startTime,
          duration: entry.duration,
        });
      }
    });

    po.observe({ entryTypes: ['longtask'] });
  } catch {
    // 일부 환경에서 예외 가능
  }
}

이 데이터를 Sentry, Datadog RUM, 자체 로그 수집으로 보내면 “특정 배포 이후 Long Task가 늘었는지”를 빠르게 감지할 수 있습니다.

INP 측정: web-vitals 사용

import { onINP } from 'web-vitals';

onINP((metric) => {
  // metric.value: INP(ms)
  // metric.attribution: 어떤 이벤트/요소가 기여했는지(가능한 경우)
  console.log('INP', metric.value, metric);
});

팁:

  • INP와 Long Task를 같은 세션/페이지뷰 키로 묶어두면, “INP가 나쁜 세션에서 Long Task가 실제로 늘었는지” 상관관계를 잡기 쉽습니다.

6단계: DevTools에서 원인 범위를 좁히는 실전 팁

Performance에서 스크립트 URL/번들 구간을 본다

Long Task가 특정 번들 파일에서 집중적으로 발생하면:

  • 최근 추가된 기능 플래그
  • 특정 페이지에서만 로드되는 chunk
  • 서드파티 태그

가 원인일 확률이 큽니다.

Coverage로 “불필요한 코드 실행”을 의심한다

상호작용 직후 실행되는 코드가 사실상 필요 없는 초기화/폴리필/분석 코드라면, 지연 로딩 또는 상호작용 이후 유휴 시간으로 미루는 게 효과적입니다.

PerformanceBottom-up에서 “Self time”이 큰 함수를 우선 제거

누적 시간이 큰 것보다, 특정 함수의 Self time이 큰 경우가 “한 방에 Long Task를 만드는 진짜 범인”일 때가 많습니다.


배포/운영 관점: INP 급락을 장애처럼 다루는 법

INP 급락은 서버 장애처럼 5xx를 만들진 않지만, 전환율/이탈률에 즉시 영향을 줍니다. 운영 관점에서는 다음이 유용합니다.

  • 배포 단위로 INP p75/p95 변화 감시
  • 특정 라우트/디바이스/브라우저 버전별 분해
  • 서드파티 스크립트 변경 이력(태그 매니저 포함) 추적

이런 “진단 루틴”은 인프라 장애를 추적하는 방식과 유사합니다. 예를 들어 원인 범위를 빠르게 좁히는 사고 대응 습관은 아래 글의 접근과도 통합니다.


체크리스트: INP 급락 시 바로 해볼 것

  • DevTools Performance로 문제 상호작용을 녹화했는가
  • Main 트랙에 50ms 이상 작업이 상호작용 근처에 존재하는가
  • Bottom-up에서 상위 원인 함수/스크립트가 식별되는가
  • 작업을 청크로 쪼개거나 Worker로 이동할 여지가 있는가
  • web-vitals로 INP를, longtask로 Long Task를 계측해 회귀를 감지하는가

마무리

Chrome INP 급락은 대부분 “사용자 입력이 들어왔을 때 메인 스레드가 이미 바쁘다”는 신호입니다. Long Task를 먼저 찾고, 해당 작업을 쪼개거나(프레임 양보), 아예 메인 스레드 밖으로 옮기면(Worker) INP는 체감될 정도로 개선되는 경우가 많습니다.

중요한 것은 한 번 고치는 데서 끝내지 않고, 계측으로 회귀를 막는 것입니다. INP와 Long Task를 함께 수집해두면, 다음 급락 때는 훨씬 빠르게 “어느 배포/어느 코드/어느 상호작용”이 원인인지 특정할 수 있습니다.