- Published on
Chrome Long Task 경고 줄여 INP 최적화하기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 성능이 아무리 좋아도, 사용자가 버튼을 눌렀을 때 화면이 멈칫하면 체감 품질은 급격히 떨어집니다. Chrome DevTools에서 자주 보이는 Long Task(50ms 이상 메인 스레드 점유) 경고는 이런 “멈칫”의 전형적인 원인이고, Core Web Vitals의 INP(Interaction to Next Paint) 를 악화시키는 가장 흔한 신호입니다.
이 글에서는 Long Task 경고를 단순히 “줄이는” 수준이 아니라, INP를 실제로 개선하기 위해 무엇을 측정하고, 어떤 순서로 분해·지연·오프로딩(Offload)해야 하는지 실전 관점에서 정리합니다.
INP와 Long Task의 관계를 정확히 이해하기
INP가 측정하는 것
INP는 사용자의 상호작용(클릭/탭/키 입력 등)에 대해 브라우저가 다음 페인트(Next Paint) 를 완료하기까지 걸린 시간을 측정합니다. 즉, 단순히 이벤트 핸들러 실행 시간만이 아니라:
- 입력 이벤트가 큐에 쌓여 대기하는 시간(메인 스레드가 바쁘면 대기)
- 이벤트 핸들러 실행 시간
- 렌더링(스타일 계산/레이아웃/페인트)까지 포함
이 모두가 합쳐져 INP를 만듭니다.
Long Task가 왜 위험 신호인가
Long Task는 메인 스레드를 50ms 이상 독점한 작업입니다. 이 작업이 있는 동안:
- 입력 이벤트가 처리되지 못하고 대기합니다(입력 지연 증가)
- 프레임이 드랍됩니다(스크롤/애니메이션 끊김)
- 렌더링이 밀립니다(Next Paint 지연)
즉 Long Task는 INP의 ‘대기 시간’과 ‘렌더링 지연’을 동시에 키우는 트리거입니다.
진단 1: DevTools에서 “진짜 원인”을 잡는 방법
Performance 패널에서 확인할 것
- Performance 기록을 켜고, 문제 상호작용(클릭/입력)을 재현합니다.
- Main 트랙에서 빨간 삼각형(긴 작업) 을 찾습니다.
- 해당 구간을 확대해서 다음을 확인합니다.
- Scripting(노란색)이 긴가? → JS 실행/파싱/GC 가능성
- Rendering/ Painting(보라/초록)이 긴가? → 레이아웃 스래싱, 큰 페인트 가능성
- Event(입력 이벤트)가 언제 처리되는가? → 입력이 큐에서 오래 대기했는가?
Long Task 내부에서 흔히 보이는 패턴
- 큰 배열/객체를 한 번에 처리(정렬/필터/맵)
- JSON parse/stringify를 대량 수행
- DOM을 반복적으로 읽고 쓰며 레이아웃을 유발
- 프레임워크 렌더링이 대규모로 발생
- 불필요한 동기 작업(동기 스토리지 접근, 무거운 계산)
전략 1: “입력 핸들러”를 가장 먼저 가볍게 만든다
INP는 특정 상호작용의 최악값에 민감합니다. 따라서 첫 번째 최적화 타겟은:
- 클릭/탭/키 입력 핸들러
- 그 핸들러가 트리거하는 동기 작업
원칙: 입력 핸들러는 ‘상태 변경’까지만, 무거운 일은 뒤로
아래는 흔한 안티패턴입니다.
button.addEventListener('click', () => {
// 1) 무거운 계산
const result = expensiveCompute(bigList);
// 2) DOM 대량 업데이트
renderTable(result);
// 3) 네트워크 요청도 동기적으로 시작
fetch('/api/log', { method: 'POST', body: JSON.stringify(result) });
});
개선 방향은 “사용자에게 즉시 반응”을 먼저 만들고, 무거운 일은 다음 틱/다음 프레임/백그라운드로 넘기는 것입니다.
button.addEventListener('click', () => {
// 즉시 UI 피드백(버튼 비활성화, 로딩 표시 등)
button.disabled = true;
button.textContent = '처리 중...';
// 다음 프레임 이후에 무거운 작업 수행
requestAnimationFrame(() => {
// 필요하면 한 번 더 양보
setTimeout(() => {
const result = expensiveCompute(bigList);
renderTable(result);
button.disabled = false;
button.textContent = '완료';
}, 0);
});
});
requestAnimationFrame은 “다음 페인트 타이밍”에 맞춰 UI 갱신 기회를 확보합니다.setTimeout(0)은 큐를 한 번 비워 입력 지연을 줄이는 효과가 있습니다.
> 포인트: INP는 “다음 페인트”까지이므로, 입력 직후 UI가 한 번이라도 빨리 그려지게 만들면 체감과 지표가 함께 개선됩니다.
전략 2: Long Task를 “쪼개서” 프레임을 살린다(Chunking)
계산량이 큰 작업은 한 번에 끝내려고 하면 Long Task가 됩니다. 해결책은 작업을 작은 청크로 분해하고, 청크 사이에 메인 스레드에 양보하는 것입니다.
청크 처리 유틸(실전 템플릿)
function processInChunks(items, handler, { chunkSize = 200 } = {}) {
let index = 0;
return new Promise((resolve) => {
function run() {
const end = Math.min(index + chunkSize, items.length);
for (; index < end; index++) {
handler(items[index], index);
}
if (index < items.length) {
// 메인 스레드에 양보
setTimeout(run, 0);
} else {
resolve();
}
}
run();
});
}
// 사용 예
await processInChunks(bigList, (item) => {
// 가벼운 처리만
doSomething(item);
}, { chunkSize: 100 });
chunkSize는 기기 성능에 따라 다릅니다. 너무 크면 Long Task가 다시 생기고, 너무 작으면 오버헤드가 커집니다.- 성능 측정 후 조정하세요.
requestIdleCallback은 “보조 수단”으로
requestIdleCallback은 유휴 시간에 실행되지만, 바쁜 페이지에서는 호출이 지연될 수 있어 입력 직후 반드시 처리되어야 하는 로직에는 부적합합니다. 다만:
- 프리컴퓨팅
- 캐시 생성
- 로그 전송
같은 “늦어도 되는” 작업에는 좋습니다.
전략 3: DOM/레이아웃 비용을 줄여 렌더링 지연을 막는다
Long Task가 Scripting이 아니고 Rendering에 몰려 있다면, JS 최적화만 해서는 INP가 잘 안 내려갑니다.
레이아웃 스래싱(읽기-쓰기 반복) 제거
안티패턴:
for (const el of items) {
// 읽기
const h = el.offsetHeight;
// 쓰기(레이아웃/스타일 변경)
el.style.height = (h + 10) + 'px';
}
개선: 읽기와 쓰기를 분리합니다.
const heights = items.map(el => el.offsetHeight);
items.forEach((el, i) => {
el.style.height = (heights[i] + 10) + 'px';
});
큰 DOM 업데이트는 한 번에(배치) 처리
DocumentFragment사용- 문자열 템플릿으로 한 번에
innerHTML적용(신뢰할 수 있는 데이터일 때) - 가상 스크롤(리스트가 크면 필수)
const frag = document.createDocumentFragment();
for (const row of rows) {
const tr = document.createElement('tr');
tr.textContent = row.name;
frag.appendChild(tr);
}
tableBody.replaceChildren(frag);
전략 4: 메인 스레드 밖으로 보내기(Web Worker)
정말 무거운 계산(암호화, 대규모 파싱, 복잡한 정렬/검색)은 쪼개도 한계가 있습니다. 이때는 Web Worker로 오프로딩하는 것이 INP 관점에서 가장 강력합니다.
Worker로 계산 오프로딩 예시
worker.js
self.onmessage = (e) => {
const { list } = e.data;
// 무거운 작업
const result = list
.filter(x => x.active)
.sort((a, b) => a.score - b.score);
self.postMessage({ result });
};
main.js
const worker = new Worker('/worker.js');
button.addEventListener('click', () => {
button.disabled = true;
button.textContent = '처리 중...';
worker.postMessage({ list: bigList });
});
worker.onmessage = (e) => {
const { result } = e.data;
renderTable(result);
button.disabled = false;
button.textContent = '완료';
};
- 메인 스레드는 입력 처리/렌더링에 집중하고, 계산은 Worker가 담당합니다.
- 데이터 전달 비용(구조화 복사)을 줄이려면
ArrayBuffer전송(transferable)도 고려하세요.
전략 5: 서드파티 스크립트와 번들 전략을 재점검한다
Long Task의 큰 비중이 “Evaluate Script”, “Parse HTML”, “Compile Script”라면 앱 코드보다:
- 태그 매니저
- A/B 테스트 도구
- 광고/트래커
- 무거운 폴리필
같은 서드파티가 원인일 가능성이 큽니다.
실전 체크리스트
- 초기 로드에 꼭 필요 없는 스크립트는
defer/async또는 사용자 상호작용 후 로드 - 라우트 단위 코드 스플리팅
- 오래된 폴리필 제거(타겟 브라우저 재정의)
<script src="/critical.js" defer></script>
<script>
// 사용자가 실제로 기능을 열었을 때만 로드
document.querySelector('#open-analytics').addEventListener('click', async () => {
await import('/analytics.js');
});
</script>
측정 자동화: Long Task를 런타임에서 수집해 회귀를 막기
DevTools는 “발견”에 좋지만, 배포 후 회귀를 막으려면 런타임 수집이 필요합니다. PerformanceObserver로 Long Task를 수집할 수 있습니다.
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.duration: Long Task 지속 시간(ms)
// entry.startTime: 시작 시점
console.log('[LongTask]', {
duration: entry.duration,
startTime: entry.startTime,
name: entry.name,
});
}
});
try {
observer.observe({ entryTypes: ['longtask'] });
} catch {
// 일부 환경 예외 처리
}
}
- 이 데이터를 RUM(Real User Monitoring)으로 보내면, 특정 페이지/브라우저/기기에서만 발생하는 INP 악화를 추적할 수 있습니다.
- 백엔드 병목이 함께 의심된다면, 앱 서버/DB 관점의 타임아웃·풀 고갈 같은 문제도 같이 보세요. 예를 들어 요청 지연이 누적되면 프론트에서 로딩 상태를 유지하는 동안 불필요한 렌더링이 반복되기도 합니다. 관련해서는 Spring Boot HikariCP 풀 고갈·DB 타임아웃 10분 진단 같은 식으로 서버 병목을 빠르게 배제하는 습관이 도움이 됩니다.
최적화 우선순위(실전 적용 순서)
- 문제 상호작용 1~2개를 고른다: INP를 떨어뜨리는 최악의 인터랙션부터.
- 입력 핸들러를 즉시 반환하도록 만든다: UI 피드백 → 무거운 작업 지연.
- Long Task를 청크로 분해한다: 50ms 미만으로 쪼개기.
- 렌더링 비용을 줄인다: 레이아웃 스래싱 제거, DOM 배치 업데이트.
- 한계가 보이면 Worker로 오프로딩: 계산/파싱/정렬.
- 서드파티를 늦추거나 제거: 초기 로드 Long Task의 흔한 범인.
- 관측을 붙여 회귀를 막는다: Long Task/INP를 배포 후에도 추적.
마무리: “경고 제거”가 아니라 “입력-페인트 경로 단축”이 목표
Chrome Long Task 경고를 줄이는 목적은 단순히 콘솔을 조용하게 만드는 것이 아니라, 사용자 입력이 다음 화면 변화로 이어지는 경로를 짧게 만드는 것입니다. 입력 핸들러를 가볍게 하고, 작업을 쪼개고, 렌더링 비용을 낮추고, 필요하면 Worker로 보내면 INP는 눈에 띄게 개선됩니다.
프론트 최적화가 어느 정도 끝났는데도 체감이 남는다면, 지연의 원인이 네트워크/서버일 수 있습니다. 특히 트래픽 급증이나 리소스 고갈로 응답이 늘어지면 프론트가 불필요한 상태 업데이트를 반복하며 Long Task가 늘기도 합니다. 이런 경우 백엔드 병목을 신속히 배제하는 진단 루틴이 중요합니다. 예: Spring Boot 3 가상스레드에서 HikariCP 고갈 해결
다음 단계로는 “특정 라우트/컴포넌트에서 어떤 상호작용이 INP를 망치는지”를 RUM으로 좁히고, 그 상호작용에만 국소적으로 Worker/청크/렌더링 최적화를 적용하는 방식이 가장 비용 대비 효과가 좋습니다.