Published on

Chrome INP 200ms↓ - Long Task 추적·해결

Authors

서론

INP(Interaction to Next Paint)는 사용자가 클릭/탭/키입력 같은 상호작용을 한 시점부터, 그 결과가 다음 페인트로 화면에 반영될 때까지 걸리는 지연을 측정합니다. 기존의 FID가 “첫 입력”만 보던 것과 달리, INP는 페이지 전체 세션에서 발생한 상호작용 중 가장 나쁜(대표값) 지연을 보고하기 때문에, 특정 화면/기능에서 발생하는 Long Task(메인 스레드를 오래 점유하는 작업) 하나가 전체 점수를 망치기 쉽습니다.

Chrome에서 INP를 200ms 이하로 내리려면, “코드를 빠르게 만든다”보다 먼저 해야 할 일이 있습니다.

  1. 어떤 상호작용에서 INP가 터지는지 찾고
  2. 그 상호작용 경로에서 Long Task가 어디서 발생하는지 추적한 뒤
  3. 입력 처리/JS 실행/렌더링을 분리하고 쪼개는 구조로 바꾸는 것

이 글은 위 3단계를 실전 관점에서 정리합니다. (CLS 문제로 체감 품질이 같이 나빠진 케이스는 Chrome CLS 급증 - 폰트·이미지·광고 시프트 해결도 함께 참고하세요.)


INP를 구성하는 3가지 지연(왜 Long Task가 치명적인가)

INP는 크게 아래 구간의 합으로 이해하면 디버깅이 쉬워집니다.

  • Input delay: 이벤트가 발생했지만 메인 스레드가 바빠서 핸들러를 시작 못 한 시간
  • Processing time: 이벤트 핸들러(및 그 후속 동기 작업) 실행 시간
  • Presentation delay: 렌더 트리 계산/레이아웃/페인트 등 “화면 반영”까지의 시간

Long Task는 보통 50ms를 넘는 메인 스레드 작업을 말하며, 다음을 동시에 악화시킵니다.

  • 메인 스레드를 점유해 Input delay를 키움(클릭했는데 반응이 늦게 시작)
  • 핸들러 내부의 무거운 계산/DOM 작업으로 Processing time을 키움
  • 레이아웃 스래싱/대규모 스타일 변경으로 Presentation delay를 키움

즉, INP는 Long Task 한 방에 3구간이 같이 무너질 수 있습니다.


1) 어디서 INP가 터지는지: 필드(Real User)와 랩(Lab) 연결

필드 데이터: CrUX / RUM으로 “문제 상호작용”을 특정

INP는 사용자 기기/네트워크/입력 방식에 민감합니다. 개발 PC에서 괜찮아도 저사양 모바일에서 터지는 일이 흔합니다.

  • CrUX(Chrome UX Report)나 Search Console(Core Web Vitals)로 INP가 나쁜 URL/그룹을 먼저 좁힙니다.
  • 가능하면 RUM(Real User Monitoring)으로 상호작용 종류(click, pointerdown, keydown), 라우트, 컴포넌트/버튼 이름, 당시 Long Task 여부를 같이 보냅니다.

랩 재현: DevTools로 “그 상호작용”을 강제로 느리게 만들기

Chrome DevTools에서 재현할 때는 다음을 권장합니다.

  • Performance 패널에서 CPU 4x~6x throttling
  • 모바일 에뮬레이션 + 실제 저사양 기기 테스트
  • 문제 동작을 수행하고, 타임라인에서 **Long Task(노란색 스크립트 블록)**를 확인

2) Long Task 추적: DevTools Performance에서 보는 포인트

Performance 트레이스를 보면 정보가 너무 많아서 길을 잃기 쉽습니다. INP 관점에서 핵심만 보면 됩니다.

(1) “상호작용 이벤트”부터 시작

  • Main 트랙에서 클릭/키입력 이벤트를 찾고
  • 해당 이벤트 뒤로 이어지는 스크립트 실행이 연속적으로 길게 이어지는지 확인합니다.

(2) Long Task의 원인을 세 가지로 분류

  1. 무거운 JS 계산: 정렬/필터/파싱/암호화/대량 루프
  2. DOM/스타일/레이아웃 비용: 큰 DOM 업데이트, 레이아웃 강제 동기화
  3. 서드파티 스크립트: 태그 매니저, 광고, A/B 테스트, 분석 SDK

(3) Call Tree / Bottom-Up으로 “가장 비싼 함수”를 잡기

  • Bottom-Up: 총 소요시간이 큰 함수 순으로 보기 좋음
  • Call Tree: 어떤 경로로 호출되는지(상호작용 핸들러 → 상태 업데이트 → 렌더링) 추적

3) 코드로 Long Task를 잡는 패턴(실전 처방전)

아래는 “INP 200ms↓”를 목표로 할 때 자주 쓰는 패턴들입니다. 한두 개만 적용해도 체감이 크게 바뀌는 경우가 많습니다.


패턴 A: 입력 이벤트 핸들러를 ‘얇게’ 만들기

원칙: 이벤트 핸들러에서는

  • 상태 변경 최소화
  • 비싼 계산/DOM 작업 금지
  • 즉시 피드백(pressed 상태, 로딩 스피너 등)만 처리

예시: 클릭 핸들러에서 무거운 작업을 다음 틱으로 미루기

button.addEventListener('click', () => {
  // 1) 즉시 UI 피드백(가벼운 작업)
  button.disabled = true;
  button.textContent = '처리 중...';

  // 2) 무거운 작업은 다음 프레임/유휴 시간으로
  requestAnimationFrame(() => {
    // 렌더 한 번 보장 후
    setTimeout(() => {
      heavyWork();
      button.textContent = '완료';
    }, 0);
  });
});
  • requestAnimationFrame으로 “일단 화면 반영”을 한 번 확보
  • setTimeout(0)은 작업을 다음 매크로태스크로 보내 입력 지연을 완화

더 공격적으로는 requestIdleCallback(지원 범위 주의)로 유휴 시간에 돌리거나, Web Worker로 옮깁니다.


패턴 B: 큰 작업을 쪼개서 프레임을 양보하기(Chunking)

Long Task는 “총량”보다 “연속 점유 시간”이 문제입니다. 300ms 작업을 30ms×10으로 쪼개면 입력/렌더링이 끼어들 틈이 생깁니다.

예시: 작업 큐를 프레임 단위로 처리

function processInChunks(items, handler, chunkSize = 200) {
  let index = 0;

  function runChunk() {
    const end = Math.min(index + chunkSize, items.length);
    for (; index < end; index++) {
      handler(items[index]);
    }

    if (index < items.length) {
      // 다음 프레임으로 양보
      requestAnimationFrame(runChunk);
    }
  }

  requestAnimationFrame(runChunk);
}

// 사용 예
processInChunks(bigArray, (x) => {
  // 가벼운 단위 작업
  doSomething(x);
});

포인트:

  • 한 프레임(16.6ms) 안에 다 끝내는 것이 이상적이지만 현실적으로 어렵습니다.
  • 대신 “프레임을 주기적으로 양보”하면 INP 최악값이 크게 줄어듭니다.

패턴 C: Web Worker로 메인 스레드 계산을 분리

정렬/필터/텍스트 파싱/대규모 JSON 처리 같은 CPU 작업은 Worker가 정답인 경우가 많습니다.

예시: Worker로 무거운 계산 이동

main.js

const worker = new Worker('/worker.js');

worker.onmessage = (e) => {
  const result = e.data;
  renderResult(result);
};

button.addEventListener('click', () => {
  button.disabled = true;
  worker.postMessage({ type: 'compute', payload: bigData });
});

worker.js

self.onmessage = (e) => {
  const { type, payload } = e.data;
  if (type === 'compute') {
    const result = heavyCompute(payload);
    self.postMessage(result);
  }
};

주의:

  • Worker로 옮긴다고 렌더링 비용이 사라지진 않습니다. 하지만 Input delay/Processing time이 크게 줄어 INP 개선 폭이 큽니다.

패턴 D: 레이아웃 스래싱(강제 동기 레이아웃) 제거

INP가 나쁜 페이지에서 흔히 발견되는 것이 “읽기-쓰기-읽기” 패턴입니다.

나쁜 예: 레이아웃 값을 읽고 곧바로 쓰고 또 읽기

const h1 = el.offsetHeight;      // read (layout)
el.style.height = (h1 + 10) + 'px'; // write
const h2 = el.offsetHeight;      // read again => forced reflow

개선: 읽기/쓰기를 배치로 묶기

// 1) 읽기 먼저
const h1 = el.offsetHeight;

// 2) 쓰기는 나중에 한 번에
requestAnimationFrame(() => {
  el.style.height = (h1 + 10) + 'px';
});

추가 팁:

  • 애니메이션은 top/left/height 대신 transform/opacity 중심으로
  • DOM 업데이트는 가능한 한 한 번의 배치 업데이트

패턴 E: 서드파티 스크립트가 Long Task를 만드는 경우

광고/분석/태그 매니저는 종종 메인 스레드에서 긴 작업을 수행합니다.

실전 접근:

  • Performance에서 Long Task의 콜스택에 서드파티가 보이면, 우선순위를 조정합니다.
    • defer/async 적용
    • 초기 상호작용 이전에는 로드하지 않기(사용자 동의/첫 입력 이후 로드)
    • 필요 없는 태그 제거(가장 강력한 최적화)

예시: 첫 상호작용 이후 로딩

let loaded = false;
function load3p() {
  if (loaded) return;
  loaded = true;
  const s = document.createElement('script');
  s.src = 'https://example.com/3p.js';
  s.async = true;
  document.head.appendChild(s);
}

window.addEventListener('pointerdown', load3p, { once: true, passive: true });

4) INP 계측: PerformanceObserver로 “문제 상호작용” 로깅하기

필드에서 재현이 어렵다면, 브라우저에서 INP 관련 이벤트를 수집해 어떤 인터랙션이 문제인지 식별해야 합니다. 아래는 브라우저 내에서 성능 엔트리를 관찰하는 최소 예시입니다.

// INP/Long Task 관찰(지원 브라우저/정확한 엔트리 타입은 환경에 따라 다를 수 있음)
(function initPerfObservers() {
  // Long Task
  if ('PerformanceObserver' in window) {
    try {
      const longTaskObserver = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          // entry.duration >= 50ms 인 경우가 일반적
          console.log('[LongTask]', {
            duration: entry.duration,
            startTime: entry.startTime,
            name: entry.name,
          });
        }
      });
      longTaskObserver.observe({ entryTypes: ['longtask'] });
    } catch {}

    // Event Timing (INP 후보)
    try {
      const eventObserver = new PerformanceObserver((list) => {
        for (const e of list.getEntries()) {
          // processingStart/processingEnd 등을 활용해 병목을 추정
          console.log('[EventTiming]', {
            name: e.name, // 'click', 'keydown' 등
            duration: e.duration,
            startTime: e.startTime,
            processingStart: e.processingStart,
            processingEnd: e.processingEnd,
            target: e.target?.tagName,
          });
        }
      });
      eventObserver.observe({ entryTypes: ['event'] });
    } catch {}
  }
})();

운영에서는 console.log 대신 RUM 엔드포인트로 전송하고, 개인정보/입력값은 절대 수집하지 않도록 필터링하세요.


5) 목표를 “INP 200ms↓”로 만들기 위한 체크리스트

우선순위 1: 상호작용 경로의 Long Task 제거

  • 클릭/탭 직후 50ms+ 스크립트가 연속되는지
  • 렌더링 직전 레이아웃 강제 동기화가 있는지
  • 대량 DOM 업데이트가 한 번에 발생하는지

우선순위 2: 사용자에게 ‘즉시 피드백’ 제공

  • 버튼 pressed/disabled 상태를 먼저 반영
  • 로딩 표시를 먼저 페인트
  • 비싼 작업은 다음 틱/유휴 시간/Worker로

우선순위 3: 서드파티 격리

  • 초기 상호작용 이전 로딩 최소화
  • 필요 태그 정리
  • 실험/분석 스크립트가 메인 스레드를 점유하는지 추적

결론

INP를 200ms 아래로 내리는 핵심은 “전체를 미세 최적화”가 아니라, 가장 나쁜 상호작용을 만드는 Long Task를 정확히 찾아 제거하는 것입니다. DevTools Performance로 상호작용 이벤트부터 역추적하고, 핸들러를 얇게 만들며(즉시 UI 피드백), 큰 작업은 쪼개거나 Worker로 분리하고, 레이아웃 스래싱과 서드파티를 정리하면 체감 성능과 지표가 함께 좋아집니다.

추가로, INP 개선 과정에서 렌더링/레이아웃 변경이 많아지면 CLS가 동반 악화될 수 있으니, 필요하면 Chrome CLS 급증 - 폰트·이미지·광고 시프트 해결도 같이 점검해 “빠르지만 흔들리는 UI”를 피하는 것이 좋습니다.