- Published on
Chrome INP 급락 원인 - Long Task 5분 진단
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙 중인 서비스에서 어느 날 갑자기 INP가 급락(악화)하는 경우, 대부분은 “사용자 입력 이후 메인 스레드가 너무 오래 붙잡혀서 다음 프레임/핸들러가 늦게 실행되는 상황”입니다. 특히 Chrome은 입력 이벤트 처리 흐름을 정교하게 측정하므로, 릴리즈 직후 특정 페이지나 특정 상호작용에서 INP가 튀는 현상이 자주 발생합니다.
이 글은 “어떤 버튼/스크롤/입력에서 INP가 떨어졌는지”를 빠르게 재현하고, Long Task를 5분 안에 범주화해 원인 후보를 좁히는 실전 진단 루틴에 집중합니다.
관련해서 INP 자체를 더 깊게 개선하는 단계별 접근은 Chrome INP 느림? Long Task·TBT 7분 개선도 함께 참고하면 좋습니다. Next.js 환경에서만 유독 재현된다면 Next.js 14 Hydration mismatch 원인 10가지도 원인 분리에 도움이 됩니다.
INP 급락을 “Long Task 문제”로 의심해야 하는 신호
다음 중 2개 이상이면, 네트워크/서버보다는 프런트 메인 스레드 Long Task 가능성이 큽니다.
- 동일 API 응답시간은 변함이 없는데, 클릭/타이핑 후 화면 반응만 느려짐
- DevTools Performance에서 Main 스레드에 50ms 초과 작업 덩어리가 반복됨
- 입력 직후
click/pointerdown/keydown핸들러가 늦게 실행되거나, 실행은 되는데 렌더링이 늦음 - 특정 브라우저(Chrome)에서만 유독 INP가 악화됨(측정 방식 차이 + 스케줄링 차이)
- Lighthouse의 TBT가 같이 증가함(항상 1:1은 아니지만 강한 상관)
5분 진단 루틴: 재현 → 캡처 → 분류
아래 루틴은 “원인을 정확히 찾는다”가 아니라, 원인 후보를 1~2개 범주로 줄이는 것이 목표입니다.
1) 1분: 문제 상호작용을 딱 1개로 고정
INP는 “가장 나쁜 입력 지연”을 대표값으로 잡는 경우가 많아서, 상호작용이 섞이면 원인 파악이 느려집니다.
- 예:
상품 목록에서 필터 체크박스 클릭1개만 반복 - 예:
검색창에 10글자 연속 입력1개만 반복
가능하면 시크릿 창에서 확장 프로그램을 배제하고, 캐시 영향을 줄이기 위해 Hard Reload를 한 번 수행합니다.
2) 2분: DevTools Performance로 Long Task 확인
- Chrome DevTools
Performance탭 - Record 시작
- 문제 상호작용 2~3회 반복
- Stop
여기서 핵심은 Main 스레드 타임라인에 노란색(스크립트) 덩어리나 보라색(스타일/레이아웃) 덩어리가 길게 이어지는지입니다.
- 스크립트 Long Task: JS 실행이 길다(파싱/실행/GC 포함)
- 렌더링 Long Task: Style/Recalculate/Layout/Paint가 길다
- 혼합형: 이벤트 핸들러가 DOM을 크게 바꾸고, 그로 인해 레이아웃이 폭발
3) 2분: Long Task를 “원인 범주”로 라벨링
Long Task는 대체로 아래 4가지 중 하나로 귀결됩니다.
- 무거운 이벤트 핸들러(클릭/입력 시 동기 작업)
- 과도한 렌더링/리렌더(React 상태 변경이 폭발)
- 레이아웃 스래싱(읽기-쓰기 반복으로 강제 동기 레이아웃)
- 서드파티 스크립트/태그(광고/분석/위젯이 메인 스레드 점유)
Performance 패널에서 Long Task를 클릭하면 Bottom-Up/Call Tree로 “어떤 함수가 시간을 먹는지”가 드러납니다. 여기서 함수명이 번들 내부로 뭉개져 있으면 sourcemap을 켜고 재수집하세요.
빠른 진단을 돕는 최소 계측 코드
DevTools만으로도 충분하지만, “현장에서 재현이 어려운 INP 급락”은 사용자 환경에서만 나타나는 경우가 많습니다. 이때는 RUM(Real User Monitoring) 형태로 Long Task를 수집하면 원인 분류 속도가 확 빨라집니다.
아래 코드는 Long Task 엔트리를 관찰해, 50ms 초과 작업을 콘솔에 요약합니다.
// longtask-observer.js
export function observeLongTasks() {
if (!('PerformanceObserver' in window)) return;
try {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.duration: Long Task 지속 시간(ms)
if (entry.duration >= 50) {
console.log('[LongTask]', {
duration: Math.round(entry.duration),
startTime: Math.round(entry.startTime),
name: entry.name,
attribution: entry.attribution?.map((a) => ({
name: a.name,
containerType: a.containerType,
containerSrc: a.containerSrc,
containerId: a.containerId,
})),
});
}
}
});
observer.observe({ entryTypes: ['longtask'] });
} catch (e) {
// 일부 환경에서 longtask entryTypes 미지원 가능
}
}
Next.js라면 클라이언트에서만 실행되게 useEffect로 묶습니다.
'use client'
import { useEffect } from 'react'
import { observeLongTasks } from './longtask-observer'
export default function LongTaskProbe() {
useEffect(() => {
observeLongTasks()
}, [])
return null
}
이 계측은 “INP 자체”를 직접 계산하진 않지만, INP 급락 구간에 Long Task가 동반되는지를 확인하는 데 매우 유용합니다.
원인 범주별 “가장 흔한” 패턴과 1차 처방
이제 Performance에서 본 Long Task의 성격에 따라 빠르게 처방을 선택합니다.
1) 무거운 이벤트 핸들러: 동기 루프, JSON 처리, 정렬/필터
특징:
- 클릭 직후 JS 덩어리가 길게 1개 생김
- Call Tree에
Array.sort,JSON.parse, 큰for루프, 마크다운/하이라이트 같은 변환기가 잡힘
1차 처방:
- 입력 직후 꼭 필요하지 않은 작업은
requestIdleCallback또는setTimeout으로 분리 - 큰 데이터 정렬/필터는 Web Worker로 이동
- 한 번 계산한 결과는 메모이제이션
예: 클릭 직후 무거운 계산을 분리
button.addEventListener('click', () => {
// UI 반응 먼저
openModal();
// 무거운 작업은 다음 틱으로
setTimeout(() => {
expensiveCompute();
}, 0);
});
예: Worker로 이동
// main.js
const worker = new Worker(new URL('./worker.js', import.meta.url));
worker.onmessage = (e) => {
render(e.data);
};
function onFilterChange(payload) {
worker.postMessage(payload);
}
// worker.js
self.onmessage = (e) => {
const result = heavyFilterAndSort(e.data);
self.postMessage(result);
};
2) 과도한 렌더링/리렌더: 상태 폭발, 리스트 렌더
특징:
- 클릭 1번에 React 컴포넌트가 대량으로 리렌더
- JS Long Task 이후 Style/Layout도 연쇄적으로 길어짐
- 리스트 아이템 수가 많을수록 급격히 악화
1차 처방:
- 리스트는 가상 스크롤(virtualization)
- 상태를 더 작은 범위로 쪼개고, 불필요한 상위 상태 변경 제거
memo,useMemo,useCallback을 “측정 기반”으로 적용
예: 큰 리스트 가상화(개념 코드)
import { FixedSizeList as List } from 'react-window'
export function ProductList({ items }) {
return (
<List
height={600}
width={'100%'}
itemCount={items.length}
itemSize={72}
>
{({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
)}
</List>
)
}
3) 레이아웃 스래싱: DOM 읽기-쓰기 교차
특징:
- Performance에서
Recalculate Style/Layout이 반복적으로 끼어듦 - 스크롤/리사이즈/입력 중에 더 심해짐
- 코드에
getBoundingClientRect같은 측정과style변경이 번갈아 등장
나쁜 예(읽기-쓰기 반복):
for (const el of items) {
const top = el.getBoundingClientRect().top; // 읽기(레이아웃 강제)
el.style.transform = `translateY(${top}px)`; // 쓰기
}
개선(읽기 먼저 모으고, 쓰기 배치):
const tops = items.map((el) => el.getBoundingClientRect().top);
items.forEach((el, i) => {
el.style.transform = `translateY(${tops[i]}px)`;
});
또한 애니메이션은 가능하면 transform/opacity 중심으로 구성하고, 레이아웃을 유발하는 top/left 변경을 피하는 것이 기본입니다.
4) 서드파티 스크립트: 태그 매니저, A/B, 광고, 챗 위젯
특징:
- Call Tree에 내 코드가 아니라 외부 도메인 스크립트가 상단에 있음
- 사용자가 특정 페이지에 진입하거나 특정 위젯이 로드될 때만 INP 악화
1차 처방:
- 로드 시점을 늦추기: 사용자 입력 이후/idle 이후 로드
- 필요 페이지에서만 로드(전 페이지 공통 삽입 제거)
- 태그 매니저 컨테이너 자체를 슬림하게(불필요 태그 정리)
예: idle 이후 로드
function loadChatWidget() {
const s = document.createElement('script');
s.src = 'https://example-cdn.com/chat.js';
s.async = true;
document.head.appendChild(s);
}
if ('requestIdleCallback' in window) {
requestIdleCallback(loadChatWidget);
} else {
setTimeout(loadChatWidget, 2000);
}
“INP 급락”을 만드는 흔한 트리거 6가지
릴리즈 직후 INP가 떨어졌다면, 아래 변경이 들어갔는지 먼저 확인하는 게 빠릅니다.
- 대용량 리스트 화면 신규 오픈 또는 페이지네이션 제거
- 입력할 때마다 API 호출 및 상태 업데이트(디바운스 미적용)
- 모달/드로어 오픈 시 DOM 트리 전체를 재구성
- 스크롤 이벤트에서 동기 연산 수행(스로틀 미적용)
- 서드파티 태그 추가 또는 태그 매니저 룰 변경
- 폰트/아이콘 로딩 방식 변경으로 레이아웃 변화가 커짐
특히 input 이벤트는 빈도가 매우 높아서, 작은 비효율도 Long Task로 커지기 쉽습니다.
5분 진단 체크리스트(현장용)
아래 체크리스트를 그대로 따라 하면, “서버 문제인지” 같은 큰 오해를 줄이고 프런트 병목으로 바로 수렴할 수 있습니다.
- Performance 캡처에서 Main에 50ms 초과 Long Task가 있는가
- Long Task의 주 색이 스크립트(JS)인가, 렌더링(Style/Layout/Paint)인가
- Long Task 직전 입력 이벤트는 무엇인가(
click,pointerdown,keydown) - Call Tree 상단 함수가 내 코드인가, 서드파티인가
- 같은 상호작용을 3번 반복했을 때 패턴이 재현되는가
- 리스트/테이블/차트처럼 DOM 규모가 큰 영역이 연관되는가
마무리: “원인 규명”보다 “범주화”가 먼저다
INP 급락은 대개 한 가지 거대한 버그라기보다, 메인 스레드를 붙잡는 작업이 조금씩 누적되어 임계점을 넘는 형태로 나타납니다. 그래서 처음부터 완벽한 근본 원인을 찾기보다, Long Task를 스크립트/렌더링/서드파티 중 어디로 분류할지를 5분 안에 끝내는 것이 가장 효율적입니다.
분류만 되면 해결책은 비교적 정형화됩니다.
- 스크립트면 쪼개고(분할), 미루고(스케줄링), 옮기고(Worker)
- 렌더링이면 DOM 규모를 줄이고, 레이아웃 유발 패턴을 제거
- 서드파티면 로드 시점과 적용 범위를 통제
다음 단계로는 “실제 INP 개선”을 위해 상호작용 단위 최적화와 TBT 동반 개선을 진행하면 됩니다. 이 과정은 Chrome INP 느림? Long Task·TBT 7분 개선에 더 구체적으로 정리해두었습니다.