article
이벤트루프 우선순위 관리하기
kemut
2023. 4. 16. 21:26
Tools for Maneuvering JavaScript's Event Loop
Tools for Maneuvering JavaScript's Event Loop
Depending on the task, it's helpful to be able to schedule work throughout various parts of the event loop's lifecycle. Let's explore some of the tools available for doing it.
www.macarthur.me
- 원문입니다. (원문에는 더 많은 예시가 있습니다!)
TL;DR
- setTimeout(() => {}, 0)
- 메인 스레드의 다른 모든 작업이 처리되지 않도록 우선 순위가 높은 작업을 여러 이벤트 루프 턴에 걸쳐 분산시키고 싶다.
- queueMicrotask(() => {})
- 현재 호출 스택에 있는 작업보다 상대적으로 덜 중요한 작업이 있지만 이벤트 루프에서 다른 일이 발생하기 전에 완료되었으면 한다.
- 요청 애니메이션 프레임(() => {})
- 리페인트 주기에 맞춰 어떤 일이 일어나기를 원한다.
- requestIdleCallback(() => {})
- 완료해야 할 우선순위가 낮은 작업이 있지만 이벤트 루프에 약간의 다운타임이 있을 때마다 작업을 수행해도 괜찮다.
Task
Task Queue
- 브라우저 API의 콜백을 보관하는 default Queue
- EventLisnter를 사용할 때
- 브라우저는 이벤트가 트리거되는 즉시 task queue에 콜백을 던진다.
- 이벤트 루프가 다시 queue로 돌아오면 해당 작업은 실행을 위해 call stack으로 이동한다.
- Task Queue는 이벤트루프의 턴마다 한 번씩 실행할 작업이 있는지 확인한다.
- → 작업이 발견되면 먼저 들어온 작업 순으로 (FIFO) 실행하고 다음 작업을 확인한다.
Microtask Queue
- call stack의 모든 작업이 실행되면, 이벤트 루프에 제어권이 아직 반환되지 않는다. 대신 microtack queue에 제어권이 주어진다.
- Promise.reslove의 .then()에 있는 모든 것이 이 대기열에서 실행된다.
- 해당 대기열이 완전히 비워지기 전까지 제어권이 이벤트루프에 반환되지 않는다.→ queueMicrotask()는 UI를 차단할 수 있다.
- → 마이크로태스크 콜백은 그 자체로 microtack queue에 더 많은 콜백을 로드할 수 있기 때문에 문제가 될 수 있다.
Tools
- 우리는 이벤트루프가 동작하는 동안, 다양한 시점에 코드를 실행할 수 있다.
setTimeout 0 - 가장 빠른 미래에 실행 예약하기
- 이벤트 루프의 가장 빠른 미래 턴에 실행되도록 콜백을 대기열에 추가
let logs = []
function firstThing() {
logs.push('log #1')
}
function secondThing() {
logs.push('log #2')
}
function thirdThing() {
logs.push('log #3')
}
function emitLogs() {
console.log('Logs:', logs)
logs = []
}
document.querySelector('button').addEventListener('click', () => {
setTimeout(() => {
firstThing()
}, 0)
queueMicrotask(() => {
emitLogs()
})
secondThing()
thirdThing()
})
// Output
// 첫번째 클릭 : ['log #2', 'log #3']
// 두번째 클릭 : ['log #1', 'log #2', 'log #3']
queueMicroTask - taskQueue 작업 끝나고 실행하기
- 현재 작업이 완료된 후 다른 일이 발생하기 위해 이벤트 루프에 제어권이 반환되기 전에 약간의 코드를 실행하고 싶을 때
- → Taskqueue의 작업이 모두 끝난 뒤 실행된다.
function firstThing() {
console.log('first very important thing.')
queueMicrotask(() => {
console.log('first: send log')
})
}
function secondThing() {
console.log('second very important thing.')
queueMicrotask(() => {
console.log('second: send another log')
})
}
firstThing()
secondThing()
// Output:
// first very important thing.
// second very important thing.
// first: send log
// second: send another log
requestAnimationFrame - repaint cycle
- 브라우저의 repaint cycle에 맞춰 코드를 실행해야 할 때 유용하다.
- 이벤트 루프는 작업을 실행할 수 있는 속도로 회전하지만 대부분의 장치는 초당 60회의 속도로 화면 업데이트를 다시 칠한다.
- requestAnimationFrame()의 가장 확실한 장점은 부드러운 애니메이션을 수행할 수 있다는 것이다.
- → setTimeout이나 setInterval은 repaint cycle을 고려하지 않고 회전이 일어나기 때문에 약간의 버벅거림이 생길 수 있다.
- 이 콜백은 TML 요소에서 CSS 전환을 정교하게 처리하는 등 다른 용도로도 유용하다.
예시) 내부의 콘텐츠 양을 알 수 없는 상자를 슬라이드하여 열고 싶다..
- 과거에는 상자의 최대 높이를 상자의 실제 높이보다 높은 값으로 설정하는 트릭을 사용한 적이 있을 것이다.
requestAnimationFrame()을 사용하면 한 번에 더 정밀하게 애니메이션을 구현할 수 있다.
- 상자를 완전히 펼친다.
- 렌더링된 높이를 측정한다.
- 브라우저에서 next repaint 후, 계산된 값으로 높이 변경을 예약하여 애니메이션을 트리거한다.
<style>
.box {
/* Other box styles... */
display: none;
}
</style>
<!-- Box HTML goes here. -->
<script>
document.getElementById('button').addEventListener('click', () => {
const box = document.getElementById('box');
// box display none 해제
box.style.display = '';
// 렌더링된 높이를 측정한다.
const height = `${box.clientHeight}px`;
// 시작 height는 0이다.
box.style.height = '0px';
// next repaint 직전
requestAnimationFrame(() => {
// next repaint 직후
requestAnimationFrame(() => {
box.style.height = height;
});
});
});
</script>
requestIdelCallback
- 이 방법은 브라우저가 "idle"로 간주될 때
- 향후 이벤트 루프가 전환될 때마다 우선순위가 낮은 코드를 실행하려고 할 때
- 이벤트 루프의 어느 턴에서 콜백이 실행될지 알 수 없다.
- 더 중요한 다른 작업에 우선순위를 양보할 수 있다.
- 가장 간단한 형태로, 요청IdleCallback()에 작업을 던지면 브라우저에 시간이 있을 때마다 실행을 위해 대기열에 대기한다.
- 현재 safari에서는 지원하지 않으므로, 폴리필 작성이 필요하다.
- IdleDeadline 객체로 미세 조정을 할 수도 있다.
- 콜백은 현재 유휴 기간에 남은 대략적인 시간을 나타내는 IdleDeadline 객체를 받게 된다.
- 이 기능은 여러 idle 주기에 걸쳐 분할해야 하는 대규모 작업을 예약할 때 유용하게 사용할 수 있다.
예시) 애플리케이션에서 스레드가 idle 상태일 때마다 최종적으로 보내려는 메시지 모음을 작성했다고 가정해보자.
- IdleDeadline을 사용하면 브라우저의 다운타임 동안 가능한 한 많은 메시지를 처리한 다음 남은 메시지를 다음 유휴 기간으로 미룰 수 있다.
const messages = ['first', 'second', 'third'];
function processMessage(message) {
console.log('processing:', message);
}
function processMessages(deadline) {
// We've got messages to process & time available.
while (deadline.timeRemaining() > 0 && messages.length) {
const message = messages.shift();
processMessage(message);
}
// Ran out of time. Schedule remaining messages for next time.
if (messages.length) {
requestIdleCallback(processMessages);
}
}
requestIdleCallback(processMessages);