본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Frontend

INP 최적화: 긴 작업 쪼개기와 양보

작성 2026. 10. 21.추가
목차9개 항목
출처와 참고

web.dev의 「Optimize Interaction to Next Paint」를 바탕으로, scheduler.yield()·content-visibility의 지원 범위는 MDN으로 확인해 재구성한 노트다. 유휴 작업은 「Using requestIdleCallback」으로 보강했다. INP의 정의와 세 구간은 웹 성능 지표에 있다.

목표와 원인

INP는 75번째 백분위에서 200ms 이하가 목표다. 상호작용 하나의 지연은 입력 지연 + 처리 시간 + 표시 지연이고, 세 구간 모두 결국 메인 스레드가 바쁜 것이 원인이다. 브라우저의 메인 스레드 하나가 JS 실행, 스타일, 레이아웃, 페인트를 모두 맡기 때문이다(브라우저 렌더링 과정, 이벤트 루프).

긴 작업(long task) 은 메인 스레드를 50ms 넘게 잡는 작업이다. 긴 작업이 도는 동안 들어온 입력은 그것이 끝날 때까지 기다린다. Lighthouse의 TBT(Total Blocking Time)는 로딩 중 긴 작업이 50ms를 넘긴 부분의 합이고, 실험실에서 INP를 어림하는 보조 지표다.

1. 느린 상호작용을 찾는다

단계방법
필드에서 찾기RUM이 가장 좋다. web-vitals의 attribution 빌드는 어떤 요소의 어떤 상호작용이, 페이지 로딩 중이었나 후였나, 어느 구간이 길었나를 알려 준다. CrUX는 요약만 준다
실험실에서 재현필드에서 찾은 흐름을 DevTools Performance 패널에서 직접 조작하며 녹화한다. 특히 페이지가 로딩되는 중에 눌러 본다. 메인 스레드가 가장 바쁜 때다
구간 나누기같은 프레임의 이벤트(pointerup, mouseup, click)를 하나로 묶어 본다. iframe은 자기 메인 스레드가 따로 있으니 어느 프레임에서 일어났는지 확인한다

느린 상호작용 하나를 고치면 다음 것이 드러난다. INP 최적화는 반복 작업이다.

2. 입력 지연을 줄인다

입력이 들어왔는데 핸들러가 시작되지 못하는 시간이다.

원인처리
시작할 때의 스크립트 평가큰 번들을 받고 파싱·컴파일·실행하는 동안 입력은 기다린다. 첫 화면에 필요 없는 JS는 코드 분할로 미룬다
타이머·fetch 응답 처리setInterval, 응답 파싱 같은 반복 작업이 입력 시점과 겹친다. 작게 나누거나 웹 워커로 옮긴다
겹치는 상호작용빠르게 타이핑할 때 앞 입력의 처리가 다음 입력을 막는다. 무거운 반응은 디바운스하거나 이전 작업을 취소한다
서드파티 스크립트광고·분석·채팅 위젯. 늦게 불러오거나 퍼사드로 바꾼다

3. 처리 시간을 줄인다

핸들러는 다음 화면에 필요한 일만

핸들러에서 다음 프레임에 보여야 할 것만 먼저 하고 나머지는 미룬다. 텍스트 편집기라면 입력한 글자를 그리는 일이 먼저고, 글자 수 세기·맞춤법 검사·저장은 뒤로 미뤄도 된다.

textarea.addEventListener('input', (event) => {
  updateTextbox(event)              // 다음 프레임에 필요한 일
 
  requestAnimationFrame(() => {     // 다음 프레임이 그려진 뒤로
    setTimeout(() => {
      updateWordCount()
      checkSpelling()
      saveChanges()
    }, 0)
  })
})

메인 스레드에 양보한다

긴 작업을 여러 작은 작업으로 나눠 그 사이에 브라우저가 입력·렌더링을 처리하게 하는 것을 양보(yield) 라고 한다.

function yieldToMain() {
  if (globalThis.scheduler?.yield) {
    return scheduler.yield()        // 이어지는 작업이 큐의 앞쪽으로 들어간다
  }
  return new Promise((resolve) => setTimeout(resolve, 0))
}
 
async function processAll(items) {
  for (const item of items) {
    process(item)
    await yieldToMain()
  }
}
방법특징
setTimeout(fn, 0)어디서나 된다. 이어지는 작업이 태스크 큐 맨 뒤로 가서 다른 작업에 밀릴 수 있다
scheduler.yield()이어지는 작업을 우선순위가 높은 큐에 넣어, 양보한 뒤 먼저 이어서 실행한다. Baseline이 아니므로 기능 감지 후 setTimeout으로 대체한다
requestAnimationFrame + setTimeout화면을 먼저 그리고 나서 나머지를 한다
requestIdleCallback(cb, { timeout })급하지 않은 일(분석 전송, 미리 계산)을 프레임이 끝나고 남는 시간이나 사용자가 쉬는 동안 실행한다. 아래 참고

유휴 콜백(requestIdleCallback) 은 콜백에 deadline을 넘긴다. deadline.timeRemaining()이 0이 될 때까지만 일하고, 남은 일은 다시 예약한다. 한 번에 주는 시간은 최대 50ms라 그 사이 들어온 입력도 100ms 안에 처리된다. 바쁜 페이지에서는 영영 불리지 않을 수 있으므로 timeout을 주면 그 시간이 지나 deadline.didTimeout이 true인 채로 불린다.

function work(deadline) {
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length) {
    sendAnalytics(tasks.shift())
  }
  if (tasks.length) requestIdleCallback(work, { timeout: 2000 })
}
requestIdleCallback(work, { timeout: 2000 })
  • 유휴 콜백 안에서 DOM을 바꾸지 않는다. 프레임이 끝난 뒤라 레이아웃이 이미 계산돼 있어, 바꾸면 그것을 무효로 만들고 소요 시간도 예측할 수 없다. DOM 변경은 DocumentFragment에 모아 다음 requestAnimationFrame에서 붙인다.

  • Safari가 지원하지 않아 Baseline이 아니다. 기능 감지 후 setTimeout으로 대체한다.

  • 매 반복마다 양보하면 오버헤드가 생긴다. 일정 시간(예: 50ms)이 지났을 때만 양보하는 식으로 조절할 수 있다. 그래도 아무 데서나 양보하는 것이 전혀 안 하는 것보다 낫다.

  • 렌더링과 무관한 계산 자체가 무겁다면 쪼개기보다 웹 워커로 메인 스레드 밖에 둔다.

레이아웃 스래싱을 피한다

스타일을 바꾼 직후 offsetHeight·getBoundingClientRect() 같은 레이아웃 값을 읽으면 브라우저가 그 자리에서 레이아웃을 계산한다(강제 동기 레이아웃). 읽기를 모으고 쓰기를 모은다. 예시는 브라우저 렌더링 과정에 있다.

4. 표시 지연을 줄인다

핸들러가 끝난 뒤 다음 프레임이 그려지기까지의 시간이다. 렌더링 작업의 양이 정한다.

원인처리
큰 DOM스타일·레이아웃 비용은 DOM 크기에 비례해 커진다. 중첩을 줄이고, 처음부터 다 만들지 말고 필요할 때 추가한다. 긴 목록은 가상화한다
화면 밖 콘텐츠까지 렌더링content-visibility: auto로 화면 밖 구역의 레이아웃·페인트를 건너뛴다. 크기 자리를 잡도록 contain-intrinsic-size를 함께 준다
JS로 큰 HTML을 한 번에 넣기innerHTML 등으로 넣은 큰 마크업은 다 파싱하고 그릴 때까지 양보하지 않는다. 서버가 스트리밍한 HTML은 받는 대로 조금씩 파싱된다. SPA에서 흔한 문제다
프레임워크 리렌더링상태 변경 하나에 넓은 트리가 다시 렌더링된다(리렌더링 원인과 추적)
.article-section {
  content-visibility: auto;
  contain-intrinsic-size: auto 500px; /* 그려진 적 있으면 그 크기를 기억한다 */
}

content-visibility는 2024년 9월 Baseline(새로 사용 가능)이 됐다. 건너뛴 내용도 페이지 내 검색·탭 이동·선택은 된다.

정리

  • INP는 메인 스레드가 얼마나 비어 있느냐의 문제다. 필드 데이터로 느린 상호작용과 구간을 먼저 찾는다.
  • 입력 지연은 시작 시 JS를 줄여서, 처리 시간은 다음 화면에 필요한 일만 하고 양보해서, 표시 지연은 렌더링할 양을 줄여서 줄인다.
  • 양보는 scheduler.yield()를 쓰고, 없으면 setTimeout으로 대체한다.
  • 무거운 계산은 쪼개기보다 웹 워커로 옮긴다.

관련 노트: 웹 성능 지표와 Lighthouse, 웹 워커로 메인 스레드 비우기, 자바스크립트 코드 분할, 이벤트 루프, 브라우저 렌더링 과정, DevTools로 성능·메모리 분석하기

원본 기록7

댓글

GitHub 계정으로 댓글을 남길 수 있어요.