정리 문서 - The-Next-Web-Research-Lab/the-next-web-research-lab.github.io GitHub Wiki

Scheduling APIs: Prioritized postTask API

https://github.com/WICG/scheduling-apis/blob/main/explainers/prioritized-post-task.md

이 글은 scheduler.postTask()라는 우선순위 지정 작업 스케줄링 API를 소개하고 있습니다. 이 API는 웹 애플리케이션에서 우선순위가 다른 작업을 효율적으로 스케줄링하고 제어하는 문제를 해결하기 위한 것입니다. 이 글에서는 웹 사이트 성능과 사용자 경험을 향상시키기 위한 작업 우선순위의 필요성을 강조하고 있습니다. 브라우저에서 작업 우선순위 설정에 대한 기존의 도전 과제도 논의되었습니다.

제안된 API는 개발자들이 "user-blocking", "user-visible", "background"과 같은 다양한 우선순위로 작업을 예약하고 제어할 수 있게 해줍니다. 작업의 우선순위는 사용자 경험과 응답성을 최적화하는 데 중요합니다. 이 새로운 API가 어떻게 작업 스케줄링을 개선할 수 있는지 설명되어 있으며, 중요성과 사용자 인식에 미치는 영향에 따라 작업의 실행 순서를 관리하기 쉽게 만들어 개발자가 작업의 중요도에 따라 효율적으로 스케줄을 조정할 수 있습니다.

주요 내용:

  • 문제: 웹 애플리케이션에서 우선순위가 다른 작업을 스케줄링하고 제어하기 위한 통합 API의 부재.
  • 해결책: scheduler.postTask() API 도입으로 개발자가 작업 우선순위를 지정할 수 있게 함.
  • 우선순위 수준: 제안된 우선순위 수준은 "user-blocking", "user-visible", "background"입니다.
  • 장점: 성능 개선, 사용자 인식 지연 감소 및 작업 스케줄링에 대한 더 나은 제어.
  • API 형태: API는 fetch와 유사하며 우선순위 및 지연과 같은 선택적 매개변수를 지원합니다.
  • 작업 컨트롤러: API는 작업을 대기열에 추가한 후에도 작업을 제어하고 수정하는 방법을 제공하며 취소 및 우선순위 변경 등을 포함합니다.
  • 보안 고려사항: 정보 유출 및 고해상도 타이밍과 관련된 잠재적인 보안 문제에 대해 논의하고 있습니다.

이 글은 또한 API의 미래 탐색 영역, 예를 들어 postTask가 아닌 비동기 작업에 우선순위를 지정하는 것, 우선순위 전파 및 상속, 타사 스크립트 동작 제어 등을 언급합니다. 작업 간의 적절한 조정을 유지하고 우선순위 역전을 피하는 중요성을 강조합니다. 게다가 API가 워커 스레드에서 사용 가능하며 대안 가능성에 대해서도 논의되었습니다.

전반적으로, scheduler.postTask() API는 웹 애플리케이션에서 작업 스케줄링을 최적화하고 사용자 경험을 향상시키기 위한 강력한 도구를 제공하려는 목적을 가지고 있습니다.

Case studies: Userspace schedulers

https://github.com/WICG/scheduling-apis/blob/main/misc/userspace-schedulers.md

이 페이지는 웹 애플리케이션의 반응성과 성능을 향상시키기 위한 사용자 공간 스케줄러에 관련한 사례 연구와 개념을 다루고 있습니다. 구체적으로 Google Maps 스케줄러와 React 스케줄러 두 가지 특정 사용자 공간 스케줄러에 초점을 맞추고 있습니다. 이러한 스케줄러는 주요 스레드 작업을 적절한 시간에 일정량씩 분할하여 응답성을 향상시키고 부드러운 프레임 속도를 유지하도록 설계되었습니다.

주요 스레드의 효과적인 스케줄링 시스템의 핵심 요소는 다음과 같이 설명됩니다:

  1. 우선순위가 있는 작업 집합: 응용 프로그램 작업 항목은 특정 우선순위 수준에서 실행될 수 있도록 예약됩니다.

  2. "가상" 작업 대기열: 이를 통해 작업 그룹의 동적 업데이트 또는 취소, 대기열에 있는 작업의 동기 실행이 가능합니다.

  3. 작업 예약을 위한 API: 미리 결정된 우선순위 수준으로 작업을 예약할 수 있는 API가 제공됩니다.

  4. 실행 루프: 사용자 및 브라우저 상태를 기반으로 작업을 적절한 시간에 실행하기 위한 메커니즘입니다.

실행 루프에서 작업을 효과적으로 예약하기 위해서는:

  • 지식: 브라우저의 렌더링 파이프라인, 다음 프레임의 타이밍, 현재 프레임의 시간 예산, 입력 및 사용자 상호작용 상태, 로딩/탐색 이벤트 등을 이해해야 합니다.

  • 조정: 주요 스레드의 다른 작업과의 효과적인 조정이 필요합니다. 이에는 데이터 가져오기, 네트워크 응답, 브라우저 콜백, 내부 브라우저 작업, 개발자 예약 콜백, 우선순위 전파, 렌더링, 상호간 읽기/쓰기 렌더링 단계 등이 포함됩니다.

작업의 우선순위 수준에는 다음과 같은 항목이 있습니다:

  1. 즉각적인 우선순위: 즉시 수행되어야 하는 긴급한 작업입니다. 이는 현재 작업 후에 진행되며 브라우저의 이벤트 루프에 양보하지 않습니다.

  2. 렌더링 차단 우선순위: 현재 프레임 내에서 반드시 수행되어야 하는 긴급한 렌더링 작업입니다. 이는 주로 requestAnimationFrame(rAF)에서 작업이 이루어지며 현재 프레임의 렌더링을 지연시킬 수 있습니다.

  3. 기본 우선순위: 다음 프레임 또는 향후 프레임을 준비하기 위해 필요한 사용자가 볼 수 있는 작업입니다. 네트워크 또는 I/O에 의존성이 있는 렌더링을 포함합니다.

  4. 유휴 우선순위: 사용자에게 보이지 않거나 사용자가 시작하지 않은 작업으로 시간이 중요하지 않은 작업입니다. 예를 들어 분석, 백업, 동기화 및 인덱싱과 같은 작업이 여기에 해당합니다.

두 가지 사례 연구가 제시됩니다:

  1. 지도 작업 스케줄러: 이 스케줄러는 원래의 프레임 속도(보통 ~60 fps)로 렌더링하려고 시도하지만 원래의 프레임 속도를 달성할 수 없는 경우에는 원래의 프레임 속도의 단위 분수(예: 1/2의 원래 속도인 ~30 fps, 1/3의 원래 속도인 ~20 fps 등)로 렌더링합니다. 모든 작업을 입력, 애니메이션, 렌더링 및 기타 단계로 분류합니다. 1, 2, 3은 각 프레임에서 수행되어야 하며 주로 requestAnimationFrame(rAF)을 통해 예약되며, 그 외의 작업은 각 프레임에서 수행될 필요가 없으며 rIC 또는 지연 방식으로 예약됩니다.

  2. React 스케줄러: 이 스케줄러는 requestAnimationFrame(rAF)와 렌더링 후 postMessage를 예약합니다. postMessage 핸들러 내에서는 시간 + 프레임 속도까지 최대한 많은 작업을 수행합니다. 작업의 우선순위 수준 대신 만료 시간을 사용하며, 프레임 속도는 동적으로 조정

Scheduling APIs

https://github.com/WICG/scheduling-apis/tree/main

모티브: 메인 스레드 경합

애플리케이션은 페이지 로드 중이나 사용자 조작 등 다양한 포인트에서 메인 스레드 경합이 발생할 수 있습니다. 이 현상은 응답성과 지연 측면에서 사용자 경험에 악영향을 미칠 수 있습니다. 예를 들어 바쁜 메인 스레드는 브라우저가 입력을 처리할 수 없도록 함으로써 응답성이 저하될 수 있습니다. 마찬가지로 작업(예: fetch 완료, 렌더링 등)에서는 경쟁 중에 큰 큐잉 시간이 발생할 수 있으며, 이로 인해 작업 지연이 증가하고 사용자 경험이 저하될 수 있습니다.

순간검색 애플리케이션을 보면 사용자가 검색 상자에 입력하면 사용자에게 반응해야 합니다. 동시에 페이지 상의 애니메이션을 원활하게 렌더링해야 하며 검색 결과 취득과 준비, 페이지 갱신 작업도 신속하게 진행해야 합니다. 앱 개발자에게는 마감일이 있으며, 메인 스레드에서 장시간 실행되는 스크립트 작업을 고칠 수 없는 상황이 발생하여 입력, 애니메이션 렌더링 또는 검색 결과 업데이트에 응답성 문제가 발생합니다.

맵 애플리케이션에서의 핀치 줌도 하나의 예입니다. 앱은 지속적으로 입력에 응답하고 렌더링을 업데이트하며 표시되는 새로운 컨텐츠를 가져와야 합니다. 순간검색 예와 마찬가지로 스크립트 작업을 장시간 수행하면 다른 작업이 차단되어 애플리케이션이 지연될 수 있습니다.

현재 솔루션, 한계 및 격차를 메우는 API

메인 스레드 경합에 대처하는 것은 스케줄링 상의 문제이며, 작업을 보다 최적의 방법으로 정렬할 수 있는 정도로 스케줄링은 긍정적인 영향을 미칠 수 있습니다. 브라우저는 사용자 입력 처리와 같은 우선 순위가 높은 작업을 수행하기 위해 작업을 사전에 수행할 수 없습니다. 이 문제는 보통 메인 스레드의 작업을 체계적으로 청크하고 스케줄링함으로써 자바스크립트 알고리즘 영역에서 해결됩니다. 긴 작업과 응답성이 모순되므로 긴 작업을 분할하여 브라우저 이벤트 루프에 양보했을 때 애플리케이션의 응답성을 유지할 수 있습니다.

자바스크립트 스케줄러 알고리즘은 사용자와 브라우저의 현재 상황과 관련하여 적절한 시간에 작업 비동기 우선 순위를 정하고 실행하는 작업 청크를 관리하도록 발전해왔습니다. 자바스크립트 스케줄러 알고리즘은 응답성을 개선하는 데 효과적이었지만 여전히 다음과 같은 몇 가지 문제가 있습니다:

1. 협력하는 행위자들 간의 조정

대부분의 자바스크립트 스케줄러 알고리즘은 사용자 경험을 향상시키는 방식으로 작업을 주문할 수 있도록 하는 우선 순위 개념을 가지고 있지만, 자바스크립트 스케줄러 알고리즘이 페이지의 모든 작업을 제어하는 것은 아니기 때문에 이는 제한적입니다.

애플리케이션은 메인 스레드와 경쟁하는 First-Party, First-Party 라이브러리, Third-Party 및 프레임워크 스크립트로 구성될 수 있습니다. 동시에 브라우저는 fetch() 및 가비지 컬렉션과 같은 메인 스레드에서 실행할 테스크도 가지고 있습니다.

공유된 우선순위 개념을 갖는 것은 브라우저가 더 나은 스케줄링 결정을 내리는 데 도움이 되고, 이는 다시 사용자 경험을 향상시키는 데 도움이 될 수 있습니다. 이 문제를 해결하기 위해 우선순위 작업 스케줄링 API를 추가할 것을 제안합니다.

2. 서로 다른 스케줄링 API 집합

스크립트 청크를 스케줄링해야 함에도 불구하고 플랫폼에는 이를 위한 통합 API가 부족합니다. 개발자는 작업을 스케줄링할 때 setTimeout, postMessage, requestAnimationFrame 또는 requestIdleCallback을 선택할 수 있습니다.

이러한 상이한 스케줄링 API 집합은 개발자가 스케줄링 코드를 작성하는 것을 더욱 어렵게 하며, 이를 위해서는 브라우저의 이벤트 루프에 대한 전문 지식이 필요합니다. 통합된 네이티브 스케줄링 API(schedular.postTask())를 생성하면 이를 완화할 수 있습니다.

3. 브라우저에 양보할 시기 결정

양보에는 오버헤드가 있습니다. 작업 게시 및 컨텍스트 전환에 따른 오버헤드, 제어권 회복 비용 등이 있습니다. 이로 인해 작업 지연 시간이 증가할 수 있습니다.

양보할 시기에 대한 현명한 결정을 내리는 것은 제한된 지식으로는 어렵습니다. 프리미티브를 스케줄링하는 것은 자바스크립트 스케줄러 알고리즘이 더 나은 결정을 내리는 데 도움이 될 수 있습니다. 예를 들어, isInputPending()isFramePending().

4. 양보한 후 제어권을 되찾는 것

청크 작업 및 양보는 응답성을 향상시키는 데 필요하지만 비용이 듭니다. 동일한 우선 순위의 임의 작업이 먼저 실행되지 않으면 결과를 산출하는 작업을 계속할 방법이 없습니다. 이는 로우 레벨 작업 지연 시간을 요구하는 스크립트에서 양보하는 것을 방해합니다. 이 비동기 자바스크립트 스케줄러 작업 모델을 고려하도록 설계된 scheduler.yield()와 같은 프리미티브를 제공하는 것이 스케줄러가 이러한 연속의 우선 순위를 더 공정하게 지정할 수 있기 때문에 도움이 될 수 있습니다.

스케줄링의 또 다른 문제

위에서 설명한 문제는 자바스크립트 스케줄링 문제의 일부만을 설명합니다. 추가적으로, 프레임이 보류 중인 시기를 감지하고 프레임 속도를 조절하며 레이아웃 스레싱을 방지하는 것과 같은 개발자의 필요성이 있습니다. 이 부분을 고려하고 있는 다른 API 중 일부는 여기에 나와 있습니다

APIs to improve JS schedulers

https://github.com/WICG/scheduling-apis/blob/main/misc/low-level-apis.md

JavaScript 스케줄러 개선을 위해 제안된 다양한 API와 잠재적인 API에 대한 내용을 다루고 있는 문서입니다. 이러한 API들은 JavaScript 애플리케이션 내에서 작업의 스케줄링과 실행을 향상시키기 위해 개발되고 있습니다. 언급된 API들은 다음과 같습니다:

  1. Is Input Pending API: 이 API는 사용자 입력이 대기 중인지 여부와 입력의 유형을 판단하는 데 사용됩니다. 이를 통해 적절한 yielding 결정을 내릴 수 있습니다. 더 많은 정보는 여기에서 확인할 수 있습니다.

  2. Is Frame Pending API: JavaScript 스케줄러는 다음 애니메이션 프레임까지의 시간을 추정하지만 브라우저 내부를 알지 못하면 정확한 추정이 어렵습니다. Is Frame Pending API는 스크립트가 대기 중인 애니메이션 프레임이 있는지 여부를 판단하여 yielding 결정을 돕는 역할을 합니다. 더 자세한 내용은 여기에서 확인할 수 있습니다.

  3. "After-paint" 콜백: 스케줄러는 종종 문서 라이프사이클 단계인 스타일, 레이아웃 및 페인트 이후에 "기본 우선순위" 작업을 즉시 실행해야 합니다. 현재 사용되는 해결책은 postMessage, 메시지 채널 및 setTimeout과 같은 메서드를 활용합니다. 이 API는 이러한 시나리오를 더 효율적으로 처리하기 위한 방법을 제공하려는 목적으로 개발되었습니다. 더 많은 정보는 여기에서 확인할 수 있습니다.

또한 문서는 미래 개발을 고려 중인 잠재적인 API도 언급하고 있으며, 이에는 다음과 같은 내용이 포함됩니다:

  • 레이아웃 이후의 클린 리드(페이즈): 이 아이디어는 DOM의 교차된 읽기와 쓰기 문제를 해결하기 위한 것으로, 레이아웃 쓰레싱을 방지합니다. 제안서는 스타일과 레이아웃이 완료된 직후에 "클린 리드"를 수행하고, 그 후에 쓰기 페이즈가 이어지도록 하는 콜백 메커니즘을 제공하려는 것입니다.

  • 비동기 작업에 대한 스케줄링 컨텍스트 전파: 이 잠재적인 API는 fetch와 promise와 같은 관련 비동기 호출 간에 스케줄링 우선순위를 상속하고 전파하는 방법을 제공하려는 목적으로 개발되었습니다. 이는 zone.js 개념에서 영감을 얻은 것입니다.

이러한 API들은 다양한 시나리오에서 JavaScript 스케줄링과 실행의 성능과 효율성을 향상시키기 위해 탐구되고 있습니다. 이 아이디어 중 일부는 아직 논의 및 탐구 단계에 있으며 공식 API 제안으로 완전히 발전되지 않았을 수 있습니다.

Prioritized Task Scheduling API

https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API

우선순위 작업 스케줄링 API

우선순위 작업 스케줄링 API는 웹 사이트 개발자 코드 또는 third-party 라이브러리 및 프레임워크에 정의된 작업이든 응용 프로그램에 속한 모든 작업의 우선순위를 지정하는 표준화된 방법을 제공합니다.

작업 우선 순위는 매우 세분화되어 있으며 작업이 사용자 상호 작용을 차단하는지 또는 사용자 경험에 영향을 미치는지 또는 백그라운드에서 실행할 수 있는지 여부를 기반으로 합니다. 개발자와 프레임워크는 API에서 정의한 광범위한 범주 내에서 보다 세분화된 우선 순위 지정 체계를 구현할 수 있습니다.

API는 Promise 기반이며 작업 우선 순위를 설정 및 변경하고, 스케줄러에 추가되는 작업을 지연하고, 작업을 중단하고, 우선 순위 변경 및 중단 이벤트를 모니터링하는 기능을 지원합니다.

이 페이지에는 다른 API 사양에 정의되었지만 작업 예약과 매우 밀접하게 관련된 navigator.scheduling.isInputPending() 메서드에 대한 정보도 포함되어 있습니다. 이 방법을 사용하면 이벤트 대기열에 대기 중인 입력 이벤트가 있는지 확인할 수 있으므로 작업 대기열을 효율적으로 처리하고 필요할 때만 기본 스레드에 양보합니다.

컨셉과 사용법

우선 순위가 지정된 작업 일정

Prioritized Task Scheduling API는 전역 개체의 scheduler 속성을 사용하여 window및 worker threads 모두에서 사용할 수 있습니다.

주요 API 메서드는 Scheduler.postTask()로, 콜백 함수("태스크")를 받아 함수의 반환 값으로 resolve하거나 오류와 함께 reject하는 promise을 반환합니다.

API의 가장 간단한 형태는 아래와 같습니다. 그러면 우선 순위가 고정되어 있고 중단할 수 없는 기본 우선 순위 user-visible이 생성됩니다.

const promise = scheduler.postTask(myTask);

메소드는 then을 사용하여 비동기적으로 해결을 기다릴 수 있는 promise를 반환하고, catch를 사용하여 태스크 콜백 함수에 의해(또는 태스크가 중단되었을 때) 발생한 오류를 캐치할 수 있습니다. 콜백 함수는 모든 종류의 함수일 수 있습니다.(아래에서는 화살표 함수를 시연합니다.)

scheduler
  .postTask(() => "Task executing")
  // Promise resolved: log task result when promise resolves
  .then((taskResult) => console.log(`${taskResult}`))
  // Promise rejected: log AbortError or errors thrown by task
  .catch((error) => console.error(`Error: ${error}`));

다음과 같이 await/async를 사용하여 동일한 작업이 대기될 수 있습니다.(참고: 이 작업은 즉시 호출 함수식(IIFE)에서 실행됩니다):

(async () => {
  try {
    const result = await scheduler.postTask(() => "Task executing");
    console.log(result);
  } catch (error) {
    // Log AbortError or error thrown in task function
    console.error(`Error: ${error}`);
  }
})();

기본 동작을 변경하려면 postTask() 메서드에 옵션 객체를 지정할 수도 있습니다. 옵션은 다음과 같습니다:

  • priority 변경 불가능한 특정 우선 순위를 지정할 수 있습니다. 한 번 설정하면 우선 순위를 변경할 수 없습니다.
  • signal 이를 통해 TaskSignal 또는 AbortSignal 중 하나인 신호를 지정할 수 있습니다. 신호는 컨트롤러와 연결되어 있으며, 이는 작업을 중단하는 데 사용될 수 있습니다. 작업이 변경 가능한 경우 TaskSignal를 사용하여 작업 우선 순위를 설정하고 변경할 수도 있습니다.
  • delay 이렇게 하면 작업이 스케줄링에 추가되기 전의 지연 시간(밀리초)을 지정할 수 있습니다.

우선 순위 옵션이 있는 위와 같은 예는 다음과 같습니다:

scheduler
  .postTask(() => "Task executing", { priority: "user-blocking" })
  .then((taskResult) => console.log(`${taskResult}`)) // Log the task result
  .catch((error) => console.error(`Error: ${error}`)); // Log any errors

작업 우선 순위

예약된 작업은 스케줄러 대기열에 추가된 순서에 따라 우선 순위로 실행됩니다.

아래에 나열된 세 가지 우선 순위(가장 높은 순서에서 가장 낮은 순서로 나열됨)는 다음과 같습니다:

user-blocking
사용자는 페이지와 상호 작용하는 것을 중지하는 작업. 여기에는 페이지를 사용할 수 있는 지점까지 렌더링하거나 사용자 입력에 응답하는 작업이 포함됩니다.

user-visible
사용자가 볼 수 있지만 사용자 작업을 반드시 차단할 필요는 없는 작업입니다. 여기에는 페이지의 비필수적인 부분(예: 비필수적인 이미지 또는 애니메이션)을 렌더링하는 것이 포함될 수 있습니다.

이것이 기본 우선 순위입니다.

background
시간에 중요하지 않는 작업입니다. 여기에는 렌더링에 필요하지 않는 로그 처리 또는 third-party 라이브러리 초기화가 포함될 수 있습니다.

가변 및 불변 작업 우선 순위

작업 우선순위를 변경할 필요가 없는 많은 사용 사례가 있지만 다른 경우에는 변경해야 합니다. 예를 들어, carousel이 뷰 영역으로 스크롤되면 이미지 가져오기가 background 태스크에서 user-visible로 변경될 수 있습니다.

태스크 우선순위는 Scheduler.postTask()로 전달되는 인수에 따라 스태틱(불변) 또는 다이내믹(변경 가능)으로 설정할 수 있습니다.

options.priority 인수에 값이 지정되어 있는 경우 작업 우선순위는 불변입니다. 지정된 값은 작업 우선순위에 사용되며 변경할 수 없습니다.

우선순위는 options.signal 인수에 options.priority가 전달되고 TaskSignal이 설정되지 않는 경우에만 변경 가능합니다. 이 경우 작업은 signal priority로부터 초기 priority를 취득한 다음 신호와 관련된 컨트롤러상에서 TaskController.setPriority()을 호출하여 우선순위를 변경할 수 있습니다.

우선순위가 options.priority로 설정되어 있지 않은 경우 또는 TaskSignaloptions.signal에 전달함으로써 우선순위가 설정되어 있지 않은 경우는 기본적으로 user-visible이 됩니다(정의상 불변합니다).

중단할 필요가 있는 작업에서는 options.signalTaskSignal 또는 AbortSignal 중 하나로 설정해야 한다는 점에 주의하십시오. 단 불변의 우선순위를 가진 작업의 경우 AbortSignal은 신호를 사용하여 작업 우선순위를 변경할 수 없음을 보다 명확하게 보여줍니다.

isInputPending()

isInputPending() API는 작업 실행을 돕기 위한 것으로, 사용자가 임의의 간격으로 앱과 상호 작용하려고 할 때만 메인 스레드를 양보하여 작업 실행자를 더 효율적으로 만들 수 있습니다.

예를 들어 이것이 무엇을 의미하는지 설명해 보겠습니다. 우선순위가 거의 같은 여러 작업이 있을 경우 유지 보수, 디버깅 및 기타 여러 가지 이유를 지원하기 위해 별도의 기능으로 분해하는 것이 좋습니다.

예:

function main() {
  a();
  b();
  c();
  d();
  e();
}

그러나 이러한 구조는 메인 스레드 차단에 도움이 되지 않습니다. 5개의 작업을 모두 하나의 main function 내부에서 실행하기 때문에 브라우저는 이를 모두 하나의 작업으로 실행합니다.

이를 처리하기 위해 주기적으로 함수를 실행하여 코드가 메인 스레드에 양보하도록 도움 줄 수 있습니다. 이는 코드가 여러 작업으로 분할되고 브라우저가 UI 업데이트와 같은 우선 순위가 높은 작업을 처리할 수 있는 기회가 제공됩니다. 이 함수의 일반적인 패턴은 setTimeout()을 사용하여 실행을 별도의 작업으로 연기합니다.

function yield() {
  return new Promise((resolve) => {
    setTimeout(resolve, 0);
  });
}

이는 각 작업이 실행된 후 메인 스레드에 양보하기 위해 다음과 같은 task runner pattern 내부에서 사용할 수 있습니다:

async function main() {
  // Create an array of functions to run
  const tasks = [a, b, c, d, e];

  // Loop over the tasks
  while (tasks.length > 0) {
    // Shift the first task off the tasks array
    const task = tasks.shift();

    // Run the task
    task();

    // Yield to the main thread
    await yield();
  }
}

이렇게 하면 메인 스레드 차단 문제에 도움이 되지만 사용자가 페이지와 상호 작용하려고 할 때만 navigator.scheduling.isInputPending()를 사용하여 yield() function를 실행 할 수 있습니다.

async function main() {
  // Create an array of functions to run
  const tasks = [a, b, c, d, e];

  while (tasks.length > 0) {
    // Yield to a pending user input
    if (navigator.scheduling.isInputPending()) {
      await yield();
    } else {
      // Shift the first task off the tasks array
      const task = tasks.shift();

      // Run the task
      task();
    }
  }
}

이를 통해 사용자가 페이지와 적극적으로 상호 작용하고 있을 때 메인 스레드를 차단하는 것을 피할 수 있어 사용자 경험이 보다 원활해질 수 있습니다. 단, 필요한 경우에만 양보함으로써 처리할 사용자 입력이 없는 경우에도 현재 작업을 계속 수행할 수 있습니다. 이렇게 하면 작업이 현재 작업 이후에 예약된 다른 중요하지 않은 브라우저 시작 작업 뒤에 대기열 뒤에 배치되는 것도 방지됩니다.

Interfaces

Scheduler
예약할 우선 순위 작업을 추가하는 postTask() 방법이 포함되어 있습니다. 이 인터페이스의 인스턴스는 Window또는 WorkerGlobalScope 글로벌 객체(this.scheduler)에서 사용할 수 있습니다.

Scheduling
이벤트 큐에 보유 중인 입력 이벤트가 있는지 확인하는 isInputPending() 메서드가 포함되어 있습니다.

TaskController
작업을 중단하고 우선 순위를 변경할 수 있습니다.

TaskSignal
작업을 중단하고 필요한 경우 TaskController 객체를 사용하여 우선 순위를 변경할 수 있는 신호 객체입니다.

TaskPriorityChangeEvent
작업 우선 순위가 병결될 때 전송되는 prioritychange 이벤트의 인터페이스입니다.

참고: 작업 우선 순위를 변경할 필요가 없는 경우 TaskControllerTaskSignal 대신 AbortController 및 관련 AbortSignal를 사용할 수 있습니다.

다른 인터페이스로의 확장

Navigator.scheduling
이 속성은 Scheduling.isInputPending() 메서드를 사용하기 위한 시작점입니다.

Window.scheduler
이 속성을 Scheduler.postTask() 메서드를 사용하기 위한 엔트리 포인트입니다. 이것은 WorkerGlobalScopeWindow에 내장되어 있으며, 대부분의 스코프에서 this를 통해 Scheduler 인스턴스를 사용할 수 있게 되었습니다.

테스크 스케줄링 예제

아래의 예에서는 mylog()를 사용해 텍스트 영역에 적습니다. 로그 영역과 메서드 코드는 일반적으로 더 관련성이 높은 코드에서 주의를 기울지이 않도록 숨깁니다.

// hidden logger code - simplifies example
let log = document.getElementById("log");
function mylog(text) {
  log.textContent += `${text}\n`;
}

기능 체크

현재 스코프에 노출된 global this에서 scheduler 속성을 테스트하여 우선 순위 작업 스케줄링이 지원되는지 확인합니다.

API가 이 브라우저에서 지원되는 경우 아래 코드는 "Feature: Supported"을 출력합니다.

// Check that feature is supported
if ("scheduler" in this) {
  mylog("Feature: Supported");
} else {
  mylog("Feature: NOT Supported");
}

기본 사용법

태스크는 Scheduler.postTask()를 사용하여 게시되며, 첫 번째 인수에서 콜백 함수(태스크)를 지정하고, 태스크 우선 순위, 지연을 지정하는 데 사용될 수 있는 선택적인 두 번째 인수를 사용합니다. 메소드는 콜백 함수의 반환 값으로 해결되는 Promise를 반환하거나, 중단 오류 또는 함수에 던져진 오류 중 하나로 거부합니다.

promise를 반환하기 때문에 Scheduler.postTask()은 다른 promise과 채인으로 연결될 수 있습니다. 아래에서는 then을 사용하여 해결할 promise을 기다리는 방법을 보여줍니다. 이것은 기본 우선 순위(user-visible)를 사용합니다.

// A function that defines a task
function myTask() {
  return "Task 1: user-visible";
}

if ("scheduler" in this) {
  // Post task with default priority: 'user-visible' (no other options)
  // When the task resolves, Promise.then() logs the result.
  scheduler.postTask(myTask).then((taskResult) => mylog(`${taskResult}`));
}

이 메서드는 async function 내부의 await와 함께 사용할 수도 있습니다. 아래 코드는 이 접근 방식을 사용하여 user-blocking 작업을 기다리는 방법을 보여줍니다.

function myTask2() {
  return "Task 2: user-blocking";
}

async function runTask2() {
  const result = await scheduler.postTask(myTask2, {
    priority: "user-blocking",
  });
  mylog(result); // Logs 'Task 2: user-blocking'.
}
runTask2();

경우에 따라 완료될 때까지 전혀 기다릴 필요가 없을 수도 있습니다. 여기에 있는 많은 예제는 단순하게 작업이 실행될 때 결과를 기록합니다.

// A function that defines a task
function mytask3() {
  mylog("Task 3: user-visible");
}

if ("scheduler" in this) {
  // Post task and log result when it runs
  scheduler.postTask(mytask3);
}

아래 로그의 위의 세 가지 작업의 출력을 보여줍니다. 작업이 실행되는 순서는 우선 순위, 그리고 선언 순서에 따라 달라집니다.

Task 2: user-blocking
Task 1: user-visible
Task 3: user-visible

영구적인 우선순위

두 번째 인수 옵션에서 priority 매개 변수를 사용하여 작업 우선 순위를 설정할 수 있습니다. 이렇게 설정된 우선 순위는 변경할 수 없습니다(변경 할 수 없음).

아래에는 세 가지 작업으로 구성된 두 그룹을 보여줍니다. 마지막 작업은 기본 우선 순위를 가집니다. 실행 시 각 작업은 단순히 예상된 순서를 기록합니다(실행 순서를 표시할 필요가 없기 때문에 결과를 기다리고 있지 않습니다).

if ("scheduler" in this) {
  // three tasks, in reverse order of priority
  scheduler.postTask(() => mylog("background 1"), { priority: "background" });
  scheduler.postTask(() => mylog("user-visible 1"), { priority: "user-visible" });
  scheduler.postTask(() => mylog("user-blocking 1"), { priority: "user-blocking" });

  // three more tasks, in reverse order of priority
  scheduler.postTask(() => mylog("background 2"), { priority: "background" });
  scheduler.postTask(() => mylog("user-visible 2"), { priority: "user-visible" });
  scheduler.postTask(() => mylog("user-blocking 2"), { priority: "user-blocking" });

  // Task with default priority: user-visible
  scheduler.postTask(() => mylog("user-visible 3 (default)"));
}

아래 출력은 태스크가 우선 순위와 선언 순서로 실행되는 것을 보여줍니다.

user-blocking 1
user-blocking 2
user-visible 1
user-visible 2
user-visible 3 (default)
background 1
background 2

작업 우선 순위 변경

태스크 우선 순위는 또한 선택적인 두 번째 인수에서 postTask()로 전달된 TaskSignal로부터 그들의 초기 값을 가져올 수 있습니다. 이러한 방식으로 설정되면, 태스크 우선 순위는 신호와 연관된 컨트롤러를 사용하여 변경될 수 있습니다.

참고: 신호를 사용하여 작업 우선 순위를 설정하고 변경하는 것은 postTask()에 대한 options.priority 인자가 설정되지 않은 경우와 options.signalTaskSignal인 경우 (AbortSignal가 아닌 경우)에만 작동합니다.

아래 코드는 먼저 TaskController에서 신호의 초기 우선 순위를 user-blocking로 설정하여 TaskController() constructor를 생성하는 방법을 보여줍니다.

그런 다음 코드는 컨트롤러의 신호에 이벤트 수신기를 추가하기 위해 addEventListener()를 사용합니다(이벤트 핸들러를 추가하기 위해 TaskSignal.onprioritychange 속성을 사용할 수 있음). 이벤트 핸들러는 이벤트에 대한 previousPriority를 사용하여 원래 우선 순위를 얻고 이벤트 대상에 대한 TaskSignal.priority를 사용하여 새로운/현재 우선 순위를 얻습니다.

그런 다음 작업이 게시되어 신호를 전달한 다음 컨트롤러에서 TaskController.setPriority()를 호출하여 우선순위를 background로 즉시 변경합니다.

if ("scheduler" in this) {
  // Create a TaskController, setting its signal priority to 'user-blocking'
  const controller = new TaskController({ priority: "user-blocking" });
  
  // Listen for 'prioritychange' events on the controller's signal.
  controller.signal.addEventListener("prioritychange", (event) => {
    const previousPriority = event.previousPriority;
    const newPriority = event.target.priority;
    mylog(`Priority changed from ${previousPriority} to ${newPriority}.`);
  });

  // Post task using the controller's signal.
  // The signal priority sets the initial priority of the task
  scheduler.postTask(() => mylog("Task 1"), { signal: controller.signal });

  // Change the priority to 'background' using the controller
  controller.setPriority("background");
}

아래 출력은 우선 순위가 user-blocking에서 background로 성공적으로 변경되었음을 보여줍니다. 이 경우 우선 순위는 작업이 실행되기 전에 변경되지만 작업이 실행되는 동안에도 동일하게 변경될 수 있습니다.

Priority changed from user-blocking to background.
Task 1

작업 취소

TaskControllerAbortController를 사용하여 정확히 같은 방식으로 작업을 중단할 수 있습니다. 유일한 차이점은 작업 우선 순위도 설정하려면 TaskController를 사용해야 한다는 것입니다.

아래 코드는 컨트롤러를 생성하고 해당 신호를 작업에 전달합니다. 그러면 작업이 즉시 중단됩니다. 그러면 AbortError와 함께 promise가 거부되고 catch 블록에 걸려 기록됩니다. 또한 TaskSignal 또는 AbortSignal에서 발생한 abort 이벤트를 듣고 중단을 기록할 수도 있습니다.

if ("scheduler" in this) {
  // Declare a TaskController with default priority
  const abortTaskController = new TaskController();
  // Post task passing the controller's signal
  scheduler
    .postTask(() => mylog("Task executing"), {
       signal: abortTaskController.signal,
    })
    .then((taskResult) => mylog(`${taskResult}`))
    .catch((error) => mylog(`Error: ${error}`);

  // Abort the task
  abortTaskController.abort();
}

아래 로그에는 중단된 작업이 표시됩니다.

Error: AbortError: signal is aborted without reason.

작업 지연

postTask()options.deplay 파라미터에 정수 밀리초를 지정하여 작업을 지연시킬 수 있습니다. 이렇게 하면 setTimeout()를 사용하여 생성될 수 있는 것처럼 시간 초과 시에 작업이 우선 순위 큐에 효과적으로 추가됩니다. delay은 작업이 스케줄러에 추가되기 전까지의 최소 시간입니다. 시간이 더 길 수도 있습니다.

아래 코드는 지연과 함께 (화살표 함수로) 추가된 두 가지 작업을 보여줍니다.

if ("scheduler" in this) {
  // Post task as arrow function with delay of 2 seconds
  scheduler
    .postTask(() => "Task delayed by 2000ms", { delay: 2000 })
    .then((taskResult) => myLog(`${taskResult}`));
  scheduler
    .postTask(() => "Next task should complete in about 2000ms", { delay: 1 })
    .then((taskResult) => mylog(`${taskResult}`));
}

두 번째 문자열은 약 2초 후에 로그에 표시됩니다.

Next task should complete in about 2000ms
Task delayed by 2000ms

브라우저 지원 범위

⚠️ **GitHub.com Fallback** ⚠️