다음 페인트에 대한 상호작용 이해 (INP)

1. 소개

다음 페인트와의 상호작용 (INP)에 관해 알아볼 수 있는 대화형 데모 및 Codelab입니다.

기본 스레드의 상호작용을 보여주는 다이어그램 작업이 차단되는 동안 사용자가 입력합니다. 입력은 이러한 작업이 완료될 때까지 지연되고, 이어서 포인터업, 마우스업, 클릭 이벤트 리스너가 실행된 후 다음 프레임이 표시될 때까지 렌더링 및 페인팅 작업이 시작됩니다.

기본 요건

  • HTML 및 JavaScript 개발에 관한 지식
  • 권장사항: INP 문서 읽어보기

학습 내용

  • 사용자 상호작용의 상호작용과 이러한 상호작용의 처리가 페이지 반응성에 미치는 영향
  • 원활한 사용자 경험을 위해 지연을 줄이고 없애는 방법

필요한 항목

  • GitHub의 코드를 클론하고 npm 명령어를 실행할 수 있는 컴퓨터
  • 텍스트 편집기
  • 모든 상호작용 측정을 위한 최신 버전의 Chrome입니다.

2. 설정

코드 가져오기 및 실행

코드는 web-vitals-codelabs 저장소에 있습니다.

  1. 터미널에서 저장소를 클론합니다. git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
  2. 클론된 디렉터리(cd web-vitals-codelabs/understanding-inp)로 순회합니다.
  3. 종속 항목 설치: npm ci
  4. 웹 서버를 시작합니다. npm run start
  5. 브라우저에서 http://localhost:5173/understanding-inp/를 방문합니다.

앱 개요

페이지 상단에는 점수 카운터와 올리기 버튼이 있습니다. 반응성과 반응성의 일반적인 데모입니다.

이 Codelab용 데모 앱의 스크린샷

버튼 아래에는 4가지 측정값이 있습니다.

  • INP: 현재 INP 점수로, 일반적으로 최악의 상호작용입니다.
  • 상호작용: 가장 최근 상호작용의 점수입니다.
  • FPS: 페이지의 기본 스레드 초당 프레임 수.
  • 타이머: 버벅거림을 시각화하는 데 도움이 되는 실행 중인 타이머 애니메이션입니다.

FPS 및 타이머 항목은 상호작용을 측정하는 데 전혀 필요하지 않습니다. 이들은 반응성을 좀 더 쉽게 시각화하기 위해 추가됩니다.

사용해 보기

올리기 버튼과 상호작용하여 점수가 증가하는 것을 확인해 보세요. 증분할 때마다 INPInteraction 값이 변경되나요?

INP는 사용자가 상호작용하는 순간부터 페이지에 렌더링된 업데이트가 사용자에게 실제로 표시될 때까지 걸리는 시간을 측정합니다.

3. Chrome DevTools와의 상호작용 측정

기타 도구 > DevTools를 엽니다. 개발자 도구 메뉴에서 페이지를 마우스 오른쪽 버튼으로 클릭하고 검사를 선택하거나 단축키를 사용합니다.

상호작용을 측정하는 데 사용할 실적 패널로 전환합니다.

앱과 함께 표시된 DevTools Performance 패널 스크린샷

다음으로 성능 패널에서 상호작용을 캡처합니다.

  1. 녹화를 누릅니다.
  2. 페이지와 상호작용합니다 (올리기 버튼 누르기).
  3. 녹화를 중지합니다.

결과 타임라인에 상호작용 트랙이 표시됩니다. 왼쪽의 삼각형을 클릭하여 펼칩니다.

DevTools 성능 패널을 사용하여 상호작용을 기록하는 애니메이션 데모

두 가지 상호작용이 나타납니다. 스크롤하거나 W 키를 눌러 두 번째 위젯을 확대합니다.

DevTools Performance 패널의 스크린샷, 패널의 상호작용 위에 마우스 오버되는 커서, 짧은 상호작용 시간을 나열하는 도움말

상호작용 위로 마우스를 가져가면 처리 시간에 시간을 소비하지 않고 상호작용이 빨라졌으며 입력 지연프레젠테이션 지연의 최소 시간을 확인할 수 있습니다. 정확한 길이는 시스템 속도에 따라 달라집니다.

4. 장기 실행 이벤트 리스너

index.js 파일을 열고 이벤트 리스너 내에서 blockFor 함수의 주석 처리를 삭제합니다.

전체 코드 보기: click_block.html

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();
});

파일을 저장합니다. 서버에서 변경사항을 확인하고 페이지를 새로고침합니다.

페이지와 다시 상호작용해 보세요. 이제 상호작용이 눈에 띄게 느려집니다.

성능 trace

성능 패널에서 다시 녹화하여 어떤 모습인지 확인하세요.

성능 패널의 1초짜리 상호작용

한때 짧은 상호작용이 이제 1초 만에 끝납니다.

상호작용 위로 마우스를 가져가면 이벤트 리스너 콜백을 실행하는 데 걸리는 시간인 '처리 기간'에 거의 전적으로 소요된 시간을 알 수 있습니다. 차단 blockFor 호출은 완전히 이벤트 리스너 내에 있으므로 이 호출로 시간이 이동합니다.

5. 실험: 처리 기간

이벤트 리스너 작업을 재정렬하는 방법을 시도하여 INP에 미치는 영향을 확인하세요.

먼저 UI 업데이트

js 호출의 순서를 바꿔서 UI를 먼저 업데이트한 다음 차단하면 어떻게 될까요?

전체 코드 보기: ui_first.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

앞서 UI가 표시된 것을 보셨나요? 주문이 INP 점수에 영향을 미치나요?

트레이스를 통해 상호작용을 검토하여 차이가 있었는지 확인해 보세요.

별도의 리스너

작업을 별도의 이벤트 리스너로 이동하면 어떻게 될까요? 하나의 이벤트 리스너에서 UI를 업데이트하고 별도의 리스너에서 페이지를 차단합니다.

전체 코드 보기: two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

이제 실적 패널이 어떻게 표시되나요?

다양한 이벤트 유형

대부분의 상호작용은 포인터 또는 키 이벤트에서 마우스 오버, 포커스/흐림 처리, 그리고 beforechange 및 beforeinput과 같은 합성 이벤트에 이르기까지 다양한 유형의 이벤트를 실행합니다.

많은 실제 페이지에는 여러 다양한 이벤트에 대한 리스너가 있습니다.

이벤트 리스너의 이벤트 유형을 변경하면 어떻게 되나요? 예를 들어 click 이벤트 리스너 중 하나를 pointerup 또는 mouseup로 바꾸시겠습니까?

전체 코드 보기: diff_handlers.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

UI 업데이트 없음

이벤트 리스너에서 UI 업데이트 호출을 삭제하면 어떻게 되나요?

전체 코드 보기: no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});

6. 처리 기간 실험 결과

성능 추적: 먼저 UI 업데이트

전체 코드 보기: ui_first.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

버튼을 클릭한 모습을 기록한 실적 패널을 보면 결과가 변경되지 않았음을 확인할 수 있습니다. 차단 코드보다 UI 업데이트가 트리거되기는 했지만 브라우저는 이벤트 리스너가 완료될 때까지 화면에 표시된 내용을 실제로 업데이트하지 않았습니다. 즉, 상호작용이 완료되는 데 1초 남짓만 걸렸다는 의미입니다.

실적 패널에서의 1초 상호작용

성능 트레이스: 별도의 리스너

전체 코드 보기: two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

기능적으로는 차이가 없습니다. 상호작용에 여전히 1초가 걸립니다.

클릭 상호작용을 확대하면 실제로 click 이벤트의 결과로 서로 다른 두 함수가 호출되는 것을 확인할 수 있습니다.

예상대로 첫 번째 UI 업데이트는 놀라울 정도로 빠르게 실행되는 반면, 두 번째 작업은 1초 정도 걸립니다. 그러나 이러한 요인의 합으로 인해 최종 사용자와의 상호작용이 느린 결과로 이어집니다.

이 예에서 완료하는 데 1밀리초도 걸리지 않는 첫 번째 함수 호출을 보여주면서 길이가 1초인 상호작용을 확대한 모습

성능 trace: 다양한 이벤트 유형

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

결과는 매우 유사합니다. 상호작용은 여전히 1초입니다. 유일한 차이점은 더 짧은 UI 업데이트 전용 click 리스너가 이제 차단 pointerup 리스너 후에 실행된다는 것입니다.

이 예에서 포인터업 리스너가 시작된 후 1밀리초 미만의 클릭 이벤트 리스너가 완료되는 것을 보여줌으로써 1초 동안 지속되는 상호작용을 확대한 모습

성능 트레이스: UI 업데이트 없음

전체 코드 보기: no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});
  • 점수는 업데이트되지 않지만 페이지는 계속 업데이트됩니다.
  • 애니메이션, CSS 효과, 기본 웹 구성요소 작업 (양식 입력), 텍스트 입력, 텍스트 강조 표시는 모두 계속 업데이트됩니다.

이 경우 버튼이 활성 상태로 전환되고 다시 클릭하면 브라우저에서 페인트를 해야 합니다. 즉, 여전히 INP가 있습니다.

이벤트 리스너가 기본 스레드를 잠시 차단하여 페이지가 페인트되지 않도록 했기 때문에 상호작용에는 여전히 1초가 걸립니다.

성능 패널을 녹화하면 이전 상호작용과 거의 동일한 상호작용을 확인할 수 있습니다.

실적 패널에서의 1초 상호작용

요점

모든 이벤트 리스너에서 실행되는 모든 코드는 상호작용을 지연시킵니다.

  • 여기에는 다양한 스크립트에서 등록된 리스너와 리스너에서 실행되는 프레임워크 또는 라이브러리 코드(예: 구성요소 렌더링을 트리거하는 상태 업데이트)가 포함됩니다.
  • 자체 코드뿐만 아니라 모든 서드 파티 스크립트도 있습니다.

흔히 발생하는 문제입니다.

마지막으로, 코드가 페인트를 트리거하지 않는다고 해서 느린 이벤트 리스너가 완료되기를 기다리지 않는 것은 아닙니다.

7. 실험: 입력 지연

이벤트 리스너 외부에서 장기 실행 코드는 어떻게 되나요? 예를 들면 다음과 같습니다.

  • 늦게 로드되는 <script>가 로드 중에 무작위로 페이지를 차단한 경우
  • setInterval와 같이 주기적으로 페이지를 차단하는 API 호출

이벤트 리스너에서 blockFor를 삭제하고 setInterval()에 추가해 보세요.

전체 코드 보기: input_delay.html

setInterval(() => {
  blockFor(1000);
}, 3000);


button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

어떤 일이 일어나나요?

8. 입력 지연 실험 결과

전체 코드 보기: input_delay.html

setInterval(() => {
  blockFor(1000);
}, 3000);


button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

setInterval 차단 작업이 실행되는 동안 발생하는 버튼 클릭을 기록하면 상호작용 자체에서 차단 작업이 실행되지 않아도 상호작용이 오랫동안 실행됩니다.

이러한 장기 실행 기간을 장기 작업이라고 합니다.

DevTools에서 상호작용 위로 마우스를 가져가면 이제 상호작용 시간이 처리 시간이 아닌 입력 지연으로 인해 발생한다는 것을 확인할 수 있습니다.

주로 입력 지연으로 인해 발생하는 1초의 차단 작업, 해당 작업을 부분적으로 진행하는 상호작용, 642밀리초의 상호작용을 보여주는 DevTools Performance 패널

이것이 상호작용에 항상 영향을 미치는 것은 아닙니다. 작업이 실행 중일 때 클릭하지 않으면 운이 좋을 수도 있습니다. 이러한 '랜덤'은 재채기는 종종 문제를 일으킬 때만 치유해야 하는 악몽이 될 수 있습니다.

이를 추적하는 한 가지 방법은 긴 작업 (또는 긴 애니메이션 프레임)과 총 차단 시간을 측정하는 것입니다.

9. 느린 프레젠테이션

지금까지 입력 지연 또는 이벤트 리스너를 통해 JavaScript의 성능을 살펴보았지만, 다음 페인트 렌더링에 영향을 미치는 다른 요소는 무엇일까요?

값비싼 효과로 페이지를 업데이트하고 있습니다.

페이지 업데이트가 빠르게 진행되더라도 브라우저에서 렌더링을 위해 열심히 노력해야 할 수 있습니다.

기본 스레드에서 다음을 실행합니다.

  • 상태 변경 후 업데이트를 렌더링해야 하는 UI 프레임워크
  • DOM을 변경하거나 비용이 많이 드는 여러 CSS 쿼리 선택기를 전환하면 스타일, 레이아웃, 페인트가 많이 트리거될 수 있습니다.

기본 스레드 밖에서:

  • CSS를 사용하여 GPU 효과 강화
  • 매우 큰 고해상도 이미지 추가
  • SVG/캔버스를 사용하여 복잡한 장면 그리기

웹 렌더링의 다양한 요소에 대한 스케치

RenderingNG

다음은 웹에서 흔히 볼 수 있는 몇 가지 예입니다.

  • 링크를 클릭한 후 초기 시각적 피드백을 제공하기 위해 일시중지하지 않고 전체 DOM을 다시 빌드하는 SPA 사이트입니다.
  • 동적 사용자 인터페이스가 포함된 복잡한 검색 필터를 제공하지만 고비용의 리스너를 실행하는 검색 페이지
  • 전체 페이지의 스타일/레이아웃을 트리거하는 어두운 모드 전환

10. 실험: 프레젠테이션 지연

느린 requestAnimationFrame

requestAnimationFrame() API를 사용하여 긴 표시 지연을 시뮬레이션해 보겠습니다.

이벤트 리스너가 반환된 에 실행되도록 blockFor 호출을 requestAnimationFrame 콜백으로 이동합니다.

전체 코드 보기: Presentation_delay.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

어떤 일이 일어나나요?

11. 프레젠테이션 지연 실험 결과

전체 코드 보기: Presentation_delay.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

상호작용은 1초 동안 지속되는데 어떻게 되었을까요?

requestAnimationFrame는 다음 페인트 전에 콜백을 요청합니다. INP는 상호작용부터 다음 페인트까지의 시간을 측정하므로 requestAnimationFrameblockFor(1000)가 다음 페인트를 1초 동안 계속 차단합니다.

실적 패널에서의 1초 상호작용

하지만 다음 두 가지 사항에 유의하세요.

  • 마우스를 가져가면 모든 상호작용 시간이 '프레젠테이션 지연'으로 표시되고 있음을 확인할 수 있습니다. 이벤트 리스너가 반환된 후에 기본 스레드가 차단되기 때문입니다.
  • 기본 스레드 활동의 루트는 더 이상 클릭 이벤트가 아니라 '애니메이션 프레임 실행됨'입니다.

12. 상호작용 진단

이 테스트 페이지에서 반응성은 점수와 타이머 및 카운터 UI를 통해 매우 시각적이지만, 평균 페이지를 테스트할 때는 더 미묘합니다.

상호작용이 오랫동안 지속될 경우, 원인이 무엇인지 항상 명확하게 알 수 있는 것은 아닙니다. 현재 상태:

  • 입력 지연?
  • 이벤트 처리 기간
  • 발표가 지연되고 있나요?

원하는 모든 페이지에서 DevTools를 사용하여 응답성을 측정할 수 있습니다. 습관을 들이려면 다음 흐름을 따라 해 보세요.

  1. 평소처럼 웹을 탐색합니다.
  2. 선택사항: 웹 바이탈 확장 프로그램이 상호작용을 기록하는 동안 DevTools 콘솔을 열어둡니다.
  3. 실적이 저조한 상호작용이 있으면 다시 시도해 보세요.
  • 이를 반복할 수 없다면 콘솔 로그를 사용하여 유용한 정보를 얻으세요.
  • 반복할 수 있다면 공연 패널에 녹음하세요.

교통체증 정보 확인하기

다음 문제 중 일부를 페이지에 추가해 보세요.

전체 코드 보기: all_the_things.html

setInterval(() => {
  blockFor(1000);
}, 3000);

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();

  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

그런 다음 콘솔과 성능 패널을 사용하여 문제를 진단하세요.

13. 실험: 비동기 작업

네트워크 요청, 타이머 시작 또는 전체 상태 업데이트와 같은 상호작용 내에서 비시각적 효과를 시작할 수 있는데, 이러한 효과가 결과적으로 페이지를 업데이트하면 어떻게 될까요?

상호작용 후 다음 페인트가 렌더링되도록 허용되면 브라우저에서 실제로 새 렌더링 업데이트가 필요하지 않다고 판단하더라도 상호작용 측정이 중지됩니다.

이를 시도해 보려면 클릭 리스너에서 UI를 계속 업데이트하되 시간 제한에서 차단 작업을 실행합니다.

전체 코드 보기: timestamp_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

이제 어떻게 되나요?

14. 비동기 작업 실험 결과

전체 코드 보기: timestamp_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

1초 길이의 작업과 27밀리초의 상호작용이 현재 트레이스에서 발생합니다.

이제 UI가 업데이트된 직후 기본 스레드를 사용할 수 있으므로 상호작용이 짧습니다. 긴 차단 작업은 계속 실행되고 페인트 후 어느 정도 실행되므로 사용자는 즉각적인 UI 피드백을 받을 수 있습니다.

교훈: 삭제할 수 없다면 적어도 옮기세요!

메서드

고정된 100밀리초의 setTimeout보다 더 나은 결과가 있을까요? 여전히 코드를 최대한 빨리 실행하기를 원할 수 있습니다. 그렇지 않으면 코드를 삭제해야 합니다.

목표:

  • 상호작용은 incrementAndUpdateUI()를 실행합니다.
  • blockFor()는 최대한 빨리 실행되지만 다음 페인트를 차단하지 않습니다.
  • 따라서 '매직 시간 제한' 없이 동작을 예측할 수 있습니다.

이를 수행하는 몇 가지 방법은 다음과 같습니다.

  • setTimeout(0)
  • Promise.then()
  • requestAnimationFrame
  • requestIdleCallback
  • scheduler.postTask()

'requestPostAnimationFrame'

다음 페인트 전에 실행하려고 시도하며 일반적으로 느린 상호작용을 하는 requestAnimationFrame 자체와 달리 requestAnimationFrame + setTimeoutrequestPostAnimationFrame의 간단한 폴리필을 만들어 다음 페인트 콜백을 실행합니다.

전체 코드 보기: raf+task.html

function afterNextPaint(callback) {
  requestAnimationFrame(() => {
    setTimeout(callback, 0);
  });
}

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  afterNextPaint(() => {
    blockFor(1000);
  });
});

인체 공학적 요소의 경우 프로미스로 래핑할 수도 있습니다.

전체 코드 보기: raf+task2.html

async function nextPaint() {
  return new Promise(resolve => afterNextPaint(resolve));
}

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();

  await nextPaint();
  blockFor(1000);
});

15. 여러 상호작용 (및 격렬한 클릭)

긴 차단 해결 방법을 이동하면 도움이 될 수 있지만 이러한 긴 작업은 여전히 페이지를 차단하므로 향후 상호작용뿐만 아니라 다른 많은 페이지 애니메이션 및 업데이트에 영향을 미칩니다.

페이지의 비동기 차단 작업 버전을 다시 시도합니다 (이전 단계에서 작업 지연에 관한 고유한 버전을 만든 경우에는 자신의 버전인 경우).

전체 코드 보기: timestamp_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

여러 번 빠르게 클릭하면 어떻게 되나요?

성능 trace

클릭할 때마다 1초 길이의 작업이 대기열에 추가되어 기본 스레드가 상당한 시간 동안 차단되도록 합니다.

기본 스레드에 여러 초짜리 작업이 있어 상호작용 속도가 800ms만큼 느림

이러한 긴 작업이 새로 수신되는 클릭과 겹치면 이벤트 리스너 자체가 거의 즉시 반환되더라도 상호작용 속도가 느려집니다. 입력 지연이 있는 이전 실험과 동일한 상황을 만들었습니다. 이번에만 입력 지연이 setInterval가 아닌 이전 이벤트 리스너에서 트리거한 작업에서 발생합니다.

전략

장기 작업은 완전히 삭제하는 것이 이상적입니다.

  • 불필요한 코드, 특히 스크립트를 완전히 삭제합니다.
  • 장기 작업을 실행하지 않도록 코드를 최적화합니다.
  • 새로운 상호작용이 발생할 때 오래된 작업을 취소합니다.

16. 전략 1: 디바운스

전형적인 전략입니다. 상호작용이 빠르게 연속으로 도착하여 처리 또는 네트워크 효과에 많은 비용이 드는 경우에는 작업을 취소하고 다시 시작할 수 있도록 의도적으로 작업 시작을 지연시킵니다. 이 패턴은 자동 완성 필드와 같은 사용자 인터페이스에 유용합니다.

  • setTimeout를 사용하여 500~1,000밀리초 정도의 타이머로 고비용 작업 시작을 지연시킵니다.
  • 이때 타이머 ID를 저장합니다.
  • 새로운 상호작용이 도착하면 clearTimeout를 사용하여 이전 타이머를 취소합니다.

전체 코드 보기: debounce.html

let timer;
button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  if (timer) {
    clearTimeout(timer);
  }
  timer = setTimeout(() => {
    blockFor(1000);
  }, 1000);
});

성능 trace

여러 상호작용이지만, 그 결과로서의 단 하나의 긴 작업만 발생

여러 번의 클릭에도 불구하고 하나의 blockFor 태스크만 실행되고 1초 동안 클릭이 발생하지 않은 후에 실행됩니다. 텍스트 입력이나 여러 번의 빠른 클릭이 예상되는 항목 타겟과 같이 갑작스럽게 발생하는 상호작용의 경우 기본적으로 사용하는 이상적인 전략입니다.

17. 전략 2: 장기 실행 작업 중단

디바운스 기간이 지난 후에 추가 클릭이 발생하여 긴 작업의 중간에 착지하고 입력 지연으로 인해 매우 느린 상호작용이 될 가능성은 여전히 존재합니다.

작업 중간에 상호작용이 발생하는 경우 사용 중인 작업을 일시중지하여 새로운 상호작용이 즉시 처리되도록 하는 것이 좋습니다. 어떻게 하면 되나요?

isInputPending와 같은 API도 있지만 일반적으로 긴 작업은 청크로 분할하는 것이 좋습니다.

setTimeout 다수

1차 시도: 간단한 작업을 합니다.

전체 코드 보기: small_tasks.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  requestAnimationFrame(() => {
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
  });
});

이는 브라우저가 각 작업을 개별적으로 예약할 수 있도록 하는 방식으로 작동하며, 입력의 우선순위가 더 높아질 수 있습니다.

여러 개의 상호작용이지만 예약된 모든 작업이 여러 개의 작은 작업으로 나뉘어 있음

다섯 번의 클릭만으로 5초가 걸리던 작업을 다시 마쳤지만, 클릭당 1초짜리 작업이 10개의 100밀리초 작업으로 나누어졌습니다. 따라서 이러한 작업과 여러 상호작용이 겹쳐도 입력 지연이 100밀리초를 초과하는 상호작용은 없습니다. 브라우저는 수신 이벤트 리스너에 setTimeout 작업보다 우선 순위를 두며, 상호작용은 반응형으로 유지됩니다.

이 전략은 별도의 진입점을 예약할 때, 예를 들어 애플리케이션 로드 시간에 호출해야 하는 독립적인 기능이 다수 있을 때 특히 효과적입니다. 스크립트를 로드하고 스크립트 평가 시간에 모든 것을 실행하면 기본적으로 모든 것이 방대한 장기 작업으로 실행될 수 있습니다.

그러나 이 전략은 공유 상태를 사용하는 for 루프와 같이 긴밀하게 결합된 코드를 분리하는 데는 적합하지 않습니다.

이제 yield()에서 구매 가능

하지만 최신 asyncawait를 활용하여 '수익 포인트'를 쉽게 추가할 수 있습니다. 사용할 수 있습니다.

예를 들면 다음과 같습니다.

전체 코드 보기: yieldy.html

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldy(ms) {
  const ms_per_part = 10;
  const parts = ms / ms_per_part;
  for (let i = 0; i < parts; i++) {
    await schedulerDotYield();

    blockFor(ms_per_part);
  }
}

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();
  await blockInPiecesYieldy(1000);
});

이전과 마찬가지로 기본 스레드는 작업 청크 후에 생성되고 브라우저가 수신되는 모든 상호작용에 응답할 수 있지만 이제 필요한 것은 별도의 setTimeout가 아닌 await schedulerDotYield()이므로 for 루프 중간에도 사용할 수 있을 만큼 인체공학적입니다.

이제 AbortContoller()에서 구매 가능

이 방법은 효과가 있었지만, 새로운 상호작용이 발생해 해야 할 작업이 변경되었더라도 각 상호작용은 더 많은 작업을 예약합니다.

우리는 디바운싱 전략을 사용하여 새로운 상호작용이 발생할 때마다 이전의 타임아웃을 취소했습니다. 여기서도 비슷한 작업을 할 수 있을까요? 이를 위한 한 가지 방법은 AbortController()를 사용하는 것입니다.

전체 코드 보기: aborty.html

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldyAborty(ms, signal) {
  const parts = ms / 10;
  for (let i = 0; i < parts; i++) {
    // If AbortController has been asked to stop, abandon the current loop.
    if (signal.aborted) return;

    await schedulerDotYield();

    blockFor(10);
  }
}

let abortController = new AbortController();

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();

  abortController.abort();
  abortController = new AbortController();

  await blockInPiecesYieldyAborty(1000, abortController.signal);
});

클릭이 수신되면 blockInPiecesYieldyAborty for 루프가 시작되어 완료해야 하는 모든 작업을 실행하면서 주기적으로 기본 스레드를 생성하므로 브라우저가 새로운 상호작용에 계속 반응합니다.

두 번째 클릭이 수신되면 첫 번째 루프가 AbortController로 취소된 것으로 신고되고 새 blockInPiecesYieldyAborty 루프가 시작됩니다. 다음번에 첫 번째 루프가 다시 실행되도록 예약되면 signal.aborted가 현재 true임을 알게 되고 추가 작업 없이 즉시 반환됩니다.

이제 기본 스레드 작업이 매우 작은 조각으로 나뉘고 상호작용이 짧으며 필요한 기간 동안만 작업이 지속됩니다.

18. 결론

모든 장기 작업을 분할하면 사이트가 새로운 상호작용에 대응할 수 있습니다. 이를 통해 초기 피드백을 신속하게 제공하고 진행 중인 작업을 중단하는 등의 결정을 내릴 수 있습니다. 즉, 진입점을 별도의 작업으로 예약해야 하는 경우가 있습니다. 경우에 따라서는 'yield'를 포인트를 얻으세요.

참고사항

  • INP는 모든 상호작용을 측정합니다.
  • 각 상호작용은 입력부터 다음 페인트(사용자가 반응성을 보는 방식)까지 측정됩니다.
  • 입력 지연, 이벤트 처리 시간, 프레젠테이션 지연은 모두 상호작용 응답성에 영향을 미칩니다.
  • DevTools를 사용하여 INP 및 상호작용 분석 결과를 쉽게 측정할 수 있습니다.

전략

  • 페이지에 장기 실행 코드 (장기 작업)가 없어야 합니다.
  • 다음 페인트가 끝날 때까지 이벤트 리스너 밖으로 불필요한 코드를 이동합니다.
  • 렌더링 업데이트 자체가 브라우저에 효율적인지 확인합니다.

자세히 알아보기