[번역] 프런트엔드 웹 퍼포먼스: 필수 요소(1)

eunbinn·2022년 7월 18일
74

FE 번역

목록 보기
7/10
post-thumbnail

브라우저를 블로킹 하지 않는 방법 - 이벤트 루프, 비동기 스케줄링, 웹 워커와 예시들 🛑

원문: https://medium.com/@matthew.costello/frontend-web-performance-the-essentials-1-cb6513e1c3a1

프런트엔드 웹 퍼포먼스: 이전 글을 보고 싶으시다면 아래를 참조하세요.
브라우저 렌더링 사이클, 하드웨어 가속, 컴포지터 레이어(Compositor Layers), 도구들과 예시

브라우저는 대부분의 작업을 메인(또는 UI) 스레드에서 실행합니다. 렌더링 주기 내의 프레임 렌더링부터 이벤트 처리 및 가비지 컬렉션까지도 메인 스레드에서 실행합니다.

메인 스레드는 싱글 스레드로, 모든 것이 연속적으로 처리되어야 합니다. 즉, 메인 스레드에서 자바스크립트를 실행 중이거나 렌더링 사이클을 실행 중이거나 가비지 수집 중이면 디스플레이를 업데이트하거나 사용자 상호 작용에 반응할 수 없습니다. 무응답, 먹통이 된 페이지 또는 버벅거림은 메인 스레드를 차단한 결과이며, 특히 지속적으로 발생하는 경우 좋지 않은 사용자 경험이 됩니다.

이벤트 루프, 웹 워커와 같은 중요한 개념과 비동기 스케줄링을 사용하는 방법을 이해하면 프런트엔드 성능에 중요한 부분인 메인 스레드가 최대한 빠르게 실행되도록 할 수 있습니다.

목차

이벤트 루프 🔁

간단히 말하면 이벤트 루프를 지속적으로 대기열(emptying queue)을 비우고, 계속해서 작업(Task)이 쌓이는 모습으로 상상할 수 있습니다. 메인 스레드에서 실행해야 하는 모든 태스크, 모든 사용자 상호 작용은 이전 태스크가 완료될 때까지 이 대기열에서 순서를 기다려야 합니다.

이벤트 루프는 매크로마이크로(또는 태스크 및 작업)와 같은 다양한 수준의 태스크([역주]task는 '작업’이라고 번역할 수 있는데, 매크로·마이크로태스크 등의 용어와 일치시키기 위해 '태스크’라고 음차 번역하였습니다)를 실행합니다.

이벤트 루프 주기 동안 매크로태스크는 최대 한 개 실행될 수 있지만, 기존의 모든 마이크로태스크가 실행되며 마이크로태스크를 우선 완료하고 이 태스크를 실행합니다. 예를 들면, 프라미스 콜백은 마이크로태스크를 사용합니다. 더 많은 마이크로태스크들을 대기열에 넣는 마이크로태스크라면, 이것들까지 (무한 루프까지) 추가로 실행됩니다. 마이크로태스크가 완료된 후에 렌더링 사이클을 자유롭게 실행할 수 있습니다.

최상의 성능과 사용자 경험을 달성하기 위해서 모든 이벤트 루프 태스크는 가능한 한 가벼워야 합니다. 단순히 더 빨리 실행되도록 구현하는 것 외에도, 브라우저에 양보하기 위해 비동기 스케줄을 사용하거나, 더 적은 코드를 실행함으로써 달성할 수 있습니다(즉, 메인 스레드에서 코드를 이동시키는 것이죠.)

메인 스레드에서 🧵

브라우저에 양보하기

첫째로, 자바스크립트에서의 프라미스를 통한 비동기 코드는 실행 시 이벤트 루프를 블로킹합니다. 논블로킹 부분은 비동기 코드의 'waiting' 부분으로, 파일을 가져오기를 기다리거나 타임아웃이 발생하길 기다리는 부분입니다.

'브라우저에 양보하기'의 기본 아이디어는 코드의 긴 실행 기간을 더 작은 그룹이나 배치로 분할하는 것입니다. 비동기 스케줄링에 의해 만들어진 각 배치 처리 사이의 간격 내에서 브라우저는 이벤트 루프를 통해 다른 태스크를 실행할 수 있는 기회가 주어집니다.

이 배치들을 논블로킹으로 스케줄링 하기 위해서는 매크로태스크에 의존해야 합니다. 만약 다수의 마이크로태스트가 대기 중인 경우 대기열은 한꺼번에 소진됩니다.

블록킹을 피하기 위해 크게 세 가지 비동기 스케줄링 기능이 활용됩니다. requestAnimationFrame (RAF), setTimeoutsetImmediate 입니다. RAF는 프레임당 렌더링과 직접 관련된 태스크를 스케줄링하는 데 유용하지만, 이 글에서는 설명하고자 하는 사용 사례에 더 적합한 setTimeout 및 setImmediate에 초점을 맞출 것입니다.

setTimeout

  • 일정 시간 내에 콜백을 실행하도록 예약합니다.
  • 지연이 완료되고 이 완료가 처리되면 콜백을 매크로태스크 큐 뒤로 푸시합니다.
  • 만약 타이머가 5단계 이상으로 중첩되면(재귀적으로 setTimeout 호출), 지연은 4ms로 고정됩니다(위에서 언급한 양도를 사용한 사례의 경우, 처음 5개 이후의 모든 배치는 각각 4ms씩 지연되며, 추가 총 지연은 max(n-5)*4, 0(여기서 n은 배치 수)가 됩니다).
  • 장치의 배터리가 부족하거나 CPU의 부하가 높은 경우 등 다른 조건이 지연을 연장할 수 있습니다.
  • 타이머는 기본 운영 체제의 인터럽트에 의존하므로 약간의 자원을 필요로합니다.
  • 타이머 인터럽트는 브라우저의 타이머 콜백 요구 사항을 충족하기 위해 하드웨어가 저전력 상태에서 벗어나도록 강제할 수 있습니다(이 문제는 오래된 하드웨어의 잔재일 수 있지만).

setImmediate

  • 콜백을 매크로태스크 큐 뒤로 푸쉬하여 콜백이 '즉시' 실행되도록 스케줄합니다.
  • 타이머 사용을 피합니다.

그래서 어떤 스케줄링을 사용해야 하나요? 상황에 따라 다릅니다.

일반적으로 많은 처리량을 최고의 성능으로 달성하길 원한다면 setImmediate를 선택하는 것이 좋습니다. 태스크에 대한 처리량이 애플리케이션에 중요하지 않을 경우 setTimeout이 선호될 수 있습니다. 모든 태스크를 우선적으로 실행하는 것은 곧 아무것도 우선적으로 실행되지 않는 것과 동일하니까요.🧘

다만, 이미 알고 있을 수도 있지만 한 가지 문제가 있습니다. 어떤 브라우저도 setImmediate를 지원하지 않는다는 것이죠(웃기게도 IE에서만 제공합니다). 다행히, 이 폴리필이 setImmediate를 완전히 사용할 수 있게 해 줍니다.

배치 처리 구현하기

프라미스 기반의 setImmediate 및 setTimeout로 유틸화하여 깔끔하게 정리하겠습니다. 여기는 중첩된 콜백이 없습니다.

function waitTime(delayMs) {
  return new Promise((resolve) => setTimeout(resolve, delayMs));
}
function waitImmediate() {
  return new Promise((resolve) => setImmediate(resolve));
}

다음은 일반적인 형태로 일괄 처리를 구현하는 방법입니다. 'maxTimePerBatchMs' 와 'waitFunction'(waitImediate()/waitTime(0))은 자유롭게 선택하면 됩니다.

const maxTimePerBatchMs = 4;
async function runBatchedProcessing() {
  let batchStartTime = Date.now();
  for (let i = 0; i < batchSize; i++) {
    const batchItem = batchItems[i];
    doSomethingWithItem(batchItem);
    const currentTime = Date.now();
    const deltaTime = currentTime - batchStartTime;
    const deltaTimeExceededLimit = deltaTime > maxTimePerBatchMs;
    if (deltaTimeExceededLimit) {
      await waitFunction();
      batchStartTime = Date.now();
    }
  }
}

maxTimePerBatchMs 값은 브라우저에서 진행 중인 다른 잠재적인 태스크와 함께 배치가 실행될 때 한 프레임 내에서 모두 처리될 수 있도록 충분히 작아야 합니다. 5ms 미만을 권장하지만, 최적의 숫자를 찾기 위해서는 프로파일링 해야합니다. 각 항목의 처리 후 시간을 확인하고 있기 때문에 일괄 처리에 소요되는 총 처리 시간은 일반적으로 최대값보다 약간 더 길다는 점에 유의하십시오.

또한 밀리초 또는 마이크로초 중 어떤 것이 필요한지에 따라 각각 'Date.now()' 또는 'performance.now()'를 사용하여 현재 시간을 얻을 수 있습니다.

직접 확인해보기

먼저, 콘솔을 통해 setTimeout 지연의 영향을 이 CodePen에서 확인할 수 있습니다.

다음으로, 배치 작업이 없는 경우, setTimeout, setImmediate의 총 실행 시간 및 블로킹을 살펴봅시다. 이 예제를 실행할 때 브라우저가 아직 코드를 컴파일하거나 최적화하지 않았기 때문에 첫 번째 통과는 훨씬 느리다는 점에 주의하세요.

배치 작업이 없는 경우

이 예제를 실행하면 메인 스레드와 애니메이션이 블로킹됨을 확인할 수 있습니다.

setTimeout 배치

이 예제를 실행하면 메인 스레드와 애니메이션이 블로킹되지 않은 것을 확인 할 수 있습니다. 하지만 배치 작업이 없는 것보다 훨씬 느립니다. 심지어 '시간 초과' 될 수도 있습니다.

(지연된) 시간 초과는 각 배치 처리 사이에 큰 간격을 초래합니다. 만약 'maxTimePerBatchMs'가 더 작았다면 setTimeout의 사용량이 더 많아지므로 시간이 더욱 느려졌을 것입니다.

setImmediate 배치

이 예제를 실행하면 메인 스레드와 애니메이션이 블로킹되지 않은 것을 확인 할 수 있습니다. 하지만 배치 작업이 없는 것보다 약간 느립니다.

타임아웃을 사용하지 않고 setImmediate는 극단적인 지연 없이 훨씬 낮은 'maxTimePerBatchMs' 값을 사용할 수 있습니다. 여전히 브라우저에 양보하고 렌더링 주기와 같은 다른 작업을 위해 간격을 남겨두지만, 전체 처리가 훨씬 더 작고 효율적입니다.

결론적으로, 각 방법에 맞는 사용 사례가 있겠지만, 전체적으로 setImmediate가 가장 효율적입니다(폴리필이 필요합니다). 이러한 배치 방법은 총 처리 시간을 연장할 수 있지만, 대부분의 경우 사용자는 더 부드러운 성능을 경험할 수 있습니다.

메인 스레드 밖에서 🚀

웹 워커 사용하기

웹 워커는 애플리케이션의 응답성에 영향을 주지 않고 느린 혹은 블로킹하는 자바스크립트를 실행할 수 있는 추가 CPU 스레드의 사용을 허용합니다. UI 블로킹만 하지 않을 뿐 실행 시간은 동일합니다.

웹 워커는 교차 스레드 통신을 위해서 구조화된 복제 알고리즘을 통해 특정 타입만 지원하는 postMessage 함수에 의존해야한다는 점에서 다소 제한적입니다.

이러한 페인 포인트를 감안할 수 있고, DOM 조작이 필요하지 않은 경우 워커는 완벽한 수단이 될 수 있습니다. 흔하진 않지만 사용 사례는 다음과 같습니다.

  • 위의 사용 사례 중 일부는 WebGL(혹은 향후 출시될 WebGPU)을 통한 병렬화덕분에 GPU 사용이 더 적합할 수 있습니다. 하지만 더 복잡할 수 있죠.

postMessage를 사용하면 여전히 메인 스레드에서 시간이 걸리며, 복사될 메시지의 복잡성 및 크기에 따라 시간은 더 길어질 수 있습니다. 보통은 매우 빠르지만, 또다른 버벅거림을 야기하지 않도록 주의하세요. 간단한 JS 작업은 웹 워커의 혜택을 보지 못할 것입니다.

번개처럼 빠른 메시징을 위해서 원본 스레드에서 더 이상 전송가능한 객체(transferrables)를 사용하지 않는 한 메모리의 복사 및 전송 권한을 건너뛸 수 있습니다. 이는 큰 배열의 숫자나 이미지(예: ArrayBuffers/ImageBitmaps)에 적합합니다. 개별 메세지를 더 큰 메세지로 균형있게 일괄 처리하면 병목 현상으로 판명될 경우, postMessage를 호출하는 빈도를 줄이는 데 유용할 수 있습니다.

또한, 웹 워커 생성은 비용을 수반하며 스레드는 한정되어 있습니다. 그러니 너무 지나치게 사용하지 마세요!

백엔드 사용하기

메인 스레드에서 코드를 덜기 위한 또 다른(덜 재미있는) 옵션은 클라이언트에서 코드를 완전히 제거하고 서버로 이동시키는 것입니다. 이것은 장단점이 있는 일반적인 접근 방식이며, 우리는 프런트엔드에 초점을 맞추고 있기 때문에 이 글에 포함시키진 않겠습니다.😉

이렇게 하면 브라우저에서 블로킹을 방지할 수 있습니다. 읽어 주셔서 감사합니다. Frontend Web Performance: The Essentials[2] 시리즈도 많은 관심 부탁드립니다!

추가 자료들 📚

이벤트 루프 심화

메인 스레드 작업 최소화하기

언제 웹 워커를 사용해야할까요

setImmediate 스펙, '효율적인 스크립트 생성'

향상된 비동기 스케줄링을 위한 API 제안

2개의 댓글

comment-user-thumbnail
2022년 7월 21일

setImmediate 궁금해서 찾아보았다가 브라우저 지원 상황이 역행적인 게 신기했네요.

답글 달기
comment-user-thumbnail
2022년 7월 24일

https://www.korecmblog.com/node-js-event-loop/
관련해서 이것도 많이 도움이 되네요

답글 달기