많은 사이트와 앱에서는 실행할 스크립트가 많습니다. JavaScript는 가능한 한 빨리 실행해야 하는 경우가 많지만, 동시에 사용자를 방해해서는 안 됩니다. 사용자가 페이지를 스크롤할 때 분석 데이터를 전송하거나 사용자가 버튼을 탭할 때 DOM에 요소를 추가하면 웹 앱이 응답하지 않아 사용자 환경이 저하될 수 있습니다.
다행히 이제 도움이 되는 API(requestIdleCallback
)를 사용할 수 있습니다. requestAnimationFrame
를 채택하여 애니메이션을 적절하게 예약하고 60fps에 도달할 가능성을 극대화할 수 있었던 것과 같은 방식으로 requestIdleCallback
는 프레임 끝에 여유 시간이 있거나 사용자가 비활성 상태일 때 작업을 예약합니다. 즉, 사용자를 방해하지 않고 작업을 수행할 수 있습니다. Chrome 47부터 사용할 수 있으므로 Chrome Canary를 사용해 지금 바로 사용해 볼 수 있습니다. 이는 실험용 기능이며 사양이 아직 변경 중이므로 향후 변경될 수 있습니다.
requestIdleCallback을 사용해야 하는 이유는 무엇인가요?
필수가 아닌 작업을 직접 예약하는 것은 매우 어렵습니다. requestAnimationFrame
콜백이 실행된 후에는 스타일 계산, 레이아웃, 페인트, 기타 브라우저 내부를 실행해야 하므로 남은 프레임 시간을 정확히 파악할 수 없습니다. 홈 롤링 솔루션은 이러한 경우를 전혀 설명하지 못합니다. 사용자가 어떤 식으로든 상호작용하지 않도록 하려면 모든 종류의 상호작용 이벤트 (scroll
, touch
, click
)에 리스너를 연결해야 합니다. 이는 리스너가 기능상 필요하지 않더라도 사용자가 상호작용하고 있지 않다는 것을 확신할 수 있도록 필요한 것입니다. 반면에 브라우저는 프레임 끝에서 사용 가능한 시간이 얼마나 되는지 정확히 알고 있으므로, 사용자가 상호작용하고 있는지는 requestIdleCallback
를 통해 가능한 한 가장 효율적인 방법으로 여유 시간을 활용할 수 있는 API를 얻습니다.
좀 더 자세히 살펴보고 어떻게 활용할 수 있는지 알아보겠습니다.
requestIdleCallback 확인 중
requestIdleCallback
은 초기 단계이므로 사용하기 전에 사용 가능한지 확인해야 합니다.
if ('requestIdleCallback' in window) {
// Use requestIdleCallback to schedule work.
} else {
// Do what you’d do today.
}
shim을 실행할 수도 있으며, 이를 위해서는 setTimeout
로 돌아가야 합니다.
window.requestIdleCallback =
window.requestIdleCallback ||
function (cb) {
var start = Date.now();
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
}
});
}, 1);
}
window.cancelIdleCallback =
window.cancelIdleCallback ||
function (id) {
clearTimeout(id);
}
setTimeout
를 사용하는 것은 requestIdleCallback
처럼 유휴 시간을 알지 못하므로 좋지 않지만 requestIdleCallback
를 사용할 수 없는 경우 함수를 직접 호출하므로 이런 식으로 심는 것이 더 나쁘지 않습니다. shim을 사용하면 requestIdleCallback
을(를) 사용할 수 있을 때 통화가 무음으로 리디렉션됩니다.
하지만 지금은 그것이 존재한다고 가정하겠습니다.
requestIdleCallback 사용
requestIdleCallback
를 호출하는 것은 콜백 함수를 첫 번째 매개변수로 사용한다는 점에서 requestAnimationFrame
와 매우 유사합니다.
requestIdleCallback(myNonEssentialWork);
myNonEssentialWork
가 호출되면 남은 작업 시간을 나타내는 숫자를 반환하는 함수가 포함된 deadline
객체가 제공됩니다.
function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0)
doWorkIfNeeded();
}
timeRemaining
함수를 호출하여 최신 값을 가져올 수 있습니다. timeRemaining()
가 0을 반환해도 해야 할 작업이 더 있다면 다른 requestIdleCallback
를 예약할 수 있습니다.
function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0 && tasks.length > 0)
doWorkIfNeeded();
if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}
함수 보장
매우 바쁜 경우 어떻게 하시나요? 콜백이 호출되지 않을까 우려될 수 있습니다. requestIdleCallback
는 requestAnimationFrame
와 유사하지만 선택적 두 번째 매개변수인 Timeout 속성이 포함된 옵션 객체를 사용한다는 점에서 다릅니다. 이 제한시간이 설정되면 브라우저에서 콜백을 실행해야 하는 시간(밀리초)을 제공합니다.
// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
시간 초과로 인해 콜백이 실행되면 다음 두 가지 사항을 확인할 수 있습니다.
timeRemaining()
는 0을 반환합니다.deadline
객체의didTimeout
속성이 true가 됩니다.
didTimeout
가 true인 경우 작업을 실행하고 완료하려고 할 가능성이 높습니다.
function myNonEssentialWork (deadline) {
// Use any remaining time, or, if timed out, just run through the tasks.
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
tasks.length > 0)
doWorkIfNeeded();
if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}
이 시간 초과로 인해 서비스 중단이 발생할 수 있으므로 사용자 (작업으로 인해 앱이 응답하지 않거나 버벅거림이 될 수 있음)가 발생할 수 있으므로 이 매개변수를 신중하게 설정해야 합니다. 가능한 경우 브라우저에서 콜백을 호출할 시기를 결정합니다.
requestIdleCallback을 사용하여 분석 데이터 전송
requestIdleCallback
를 사용하여 분석 데이터를 전송하는 방법을 살펴보겠습니다. 이 경우 탐색 메뉴 탭하기와 같은 이벤트를 추적할 수 있습니다. 그러나 일반적으로 화면에 애니메이션되므로 이 이벤트를 Google 애널리틱스로 즉시 전송하지 않는 것이 좋습니다. 전송할 이벤트 배열을 생성하여 향후 특정 시점에 전송하도록 요청합니다.
var eventsToSend = [];
function onNavOpenClick () {
// Animate the menu.
menu.classList.add('open');
// Store the event for later.
eventsToSend.push(
{
category: 'button',
action: 'click',
label: 'nav',
value: 'open'
});
schedulePendingEvents();
}
이제 대기 중인 이벤트를 처리하려면 requestIdleCallback
를 사용해야 합니다.
function schedulePendingEvents() {
// Only schedule the rIC if one has not already been set.
if (isRequestIdleCallbackScheduled)
return;
isRequestIdleCallbackScheduled = true;
if ('requestIdleCallback' in window) {
// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
} else {
processPendingAnalyticsEvents();
}
}
시간 제한을 2초로 설정했지만 이 값은 애플리케이션에 따라 다릅니다. 분석 데이터의 경우 데이터가 미래의 특정 시점이 아닌 합리적인 기간 내에 보고되도록 시간 제한을 사용하는 것이 좋습니다.
마지막으로 requestIdleCallback
가 실행할 함수를 작성해야 합니다.
function processPendingAnalyticsEvents (deadline) {
// Reset the boolean so future rICs can be set.
isRequestIdleCallbackScheduled = false;
// If there is no deadline, just run as long as necessary.
// This will be the case if requestIdleCallback doesn’t exist.
if (typeof deadline === 'undefined')
deadline = { timeRemaining: function () { return Number.MAX_VALUE } };
// Go for as long as there is time remaining and work to do.
while (deadline.timeRemaining() > 0 && eventsToSend.length > 0) {
var evt = eventsToSend.pop();
ga('send', 'event',
evt.category,
evt.action,
evt.label,
evt.value);
}
// Check if there are more events still to send.
if (eventsToSend.length > 0)
schedulePendingEvents();
}
이 예시에서는 requestIdleCallback
가 존재하지 않으면 분석 데이터가 즉시 전송되어야 한다고 가정했습니다. 그러나 프로덕션 애플리케이션에서는 상호작용과 충돌하거나 버벅거림이 발생하지 않도록 시간 초과로 전송을 지연하는 것이 더 나을 수 있습니다.
requestIdleCallback을 사용하여 DOM 변경
requestIdleCallback
가 성능에 크게 도움이 되는 또 다른 상황은 필수적이지 않은 DOM 변경사항이 있는 경우입니다(예: 계속 증가하는 지연 로드 목록의 끝에 항목을 추가하는 경우). requestIdleCallback
가 실제로 일반적인 프레임에 어떻게 들어맞는지 살펴보겠습니다.
브라우저가 특정 프레임에서 콜백을 실행하기에 너무 바쁠 수 있으므로 프레임 끝에 추가 작업을 할 수 있는 여유 시간이 있을 것이라고 예상해서는 안 됩니다. 따라서 프레임별로 실행되는 setImmediate
와는 다릅니다.
콜백이 프레임 끝에서 실행되면 현재 프레임이 커밋된 후에 실행되도록 예약됩니다. 즉, 스타일 변경사항이 적용되고 특히 레이아웃이 계산됩니다. 유휴 콜백 내에서 DOM을 변경하면 레이아웃 계산이 무효화됩니다. 다음 프레임에 레이아웃 읽기가 있는 경우(예: getBoundingClientRect
, clientWidth
등에서 브라우저가 강제 동기식 레이아웃을 실행해야 하므로 잠재적 성능 병목 현상이 발생할 수 있습니다.
유휴 콜백에서 DOM 변경을 트리거하지 않는 또 다른 이유는 DOM 변경의 시간 영향을 예측할 수 없기 때문에 브라우저가 제공한 기한을 쉽게 지날 수 있기 때문입니다.
requestAnimationFrame
콜백 내에서만 DOM을 변경하는 것이 가장 좋습니다. 이러한 유형의 작업을 염두에 두고 브라우저에서 예약하기 때문입니다. 즉, 코드에서 문서 프래그먼트를 사용해야 하며 이는 다음 requestAnimationFrame
콜백에서 추가할 수 있습니다. VDOM 라이브러리를 사용하는 경우 requestIdleCallback
를 사용하여 변경할 수 있지만, 유휴 콜백이 아닌 다음 requestAnimationFrame
콜백에서 DOM 패치를 적용합니다.
이를 염두에 두고 코드를 살펴보겠습니다.
function processPendingElements (deadline) {
// If there is no deadline, just run as long as necessary.
if (typeof deadline === 'undefined')
deadline = { timeRemaining: function () { return Number.MAX_VALUE } };
if (!documentFragment)
documentFragment = document.createDocumentFragment();
// Go for as long as there is time remaining and work to do.
while (deadline.timeRemaining() > 0 && elementsToAdd.length > 0) {
// Create the element.
var elToAdd = elementsToAdd.pop();
var el = document.createElement(elToAdd.tag);
el.textContent = elToAdd.content;
// Add it to the fragment.
documentFragment.appendChild(el);
// Don't append to the document immediately, wait for the next
// requestAnimationFrame callback.
scheduleVisualUpdateIfNeeded();
}
// Check if there are more events still to send.
if (elementsToAdd.length > 0)
scheduleElementCreation();
}
여기서는 요소를 만들고 textContent
속성을 사용하여 채우지만 요소 생성 코드가 더 복잡할 가능성이 있습니다. 요소를 만든 후 scheduleVisualUpdateIfNeeded
가 호출됩니다. 이 메서드는 단일 requestAnimationFrame
콜백을 설정하고 이 콜백은 본문에 문서 프래그먼트를 추가합니다.
function scheduleVisualUpdateIfNeeded() {
if (isVisualUpdateScheduled)
return;
isVisualUpdateScheduled = true;
requestAnimationFrame(appendDocumentFragment);
}
function appendDocumentFragment() {
// Append the fragment and reset.
document.body.appendChild(documentFragment);
documentFragment = null;
}
이제 DOM에 항목을 추가할 때 발생하는 버벅거림이 훨씬 줄어듭니다. 멋집니다!
FAQ
- 폴리필이 있나요?
안타깝게도
setTimeout
에 대한 투명한 리디렉션을 원하는 경우 shim이 있습니다. 이 API가 존재하는 이유는 웹 플랫폼에 실질적인 격차를 줄 수 있기 때문입니다. 활동 부족을 추론하기는 어렵지만 프레임 끝에서 여유 시간을 파악할 수 있는 JavaScript API는 없으므로 기껏해야 추측해야 합니다.setTimeout
,setInterval
,setImmediate
와 같은 API를 사용하여 작업을 예약할 수 있지만requestIdleCallback
와 같은 방식으로 사용자 상호작용을 피하기 위해 시간이 지정되지는 않습니다. - 기한을 초과하면 어떻게 되나요?
timeRemaining()
가 0을 반환하지만 더 오래 실행하도록 선택하면 브라우저에서 작업이 중단될 염려 없이 그렇게 할 수 있습니다. 하지만 브라우저는 사용자에게 원활한 환경을 보장해야 하는 기한을 제공하므로 아주 타당한 이유가 없다면 항상 기한을 준수해야 합니다. timeRemaining()
가 반환하는 최댓값이 있나요? 예, 현재 50ms입니다. 반응형 애플리케이션을 유지 관리하려고 할 때, 사용자 상호작용에 대한 모든 응답은 100ms 미만으로 유지되어야 합니다. 사용자가 50ms의 시간 동안 상호작용하면 대부분의 경우 유휴 콜백이 완료되고 브라우저가 사용자의 상호작용에 응답할 수 있어야 합니다. 브라우저에서 실행할 시간이 충분하다고 판단하는 경우 여러 개의 유휴 콜백이 연달아 예약될 수도 있습니다.- requestIdleCallback에서 수행하지 말아야 할 작업이 있나요?
비교적 예측 가능한 특성을 가진 작은 청크 (마이크로태스크)로 작업을 수행하는 것이 이상적입니다. 예를 들어, 특히 DOM을 변경하면 실행 시간을 예측할 수 없게 됩니다. 스타일 계산, 레이아웃, 페인트, 합성을 트리거하기 때문입니다. 따라서 위에 제안된 것처럼
requestAnimationFrame
콜백에서만 DOM을 변경해야 합니다. 주의해야 할 또 다른 사항은 프로미스를 해결 (또는 거부)하는 것입니다. 콜백은 더 이상 남은 시간이 없더라도 유휴 콜백이 완료된 직후에 실행되기 때문입니다. - 프레임 끝에 항상
requestIdleCallback
이 발생하나요? 아니요, 항상 그런 것은 아닙니다. 브라우저는 프레임이 끝날 때나 사용자가 활동이 없는 시간에 여유 시간이 있을 때마다 콜백을 예약합니다. 콜백이 프레임별로 호출되리라 예상해서는 안 되며, 지정된 시간 내에 콜백이 실행되어야 한다면 제한 시간을 활용해야 합니다. requestIdleCallback
콜백을 여러 개 사용할 수 있나요? 예. 여러 개의requestAnimationFrame
콜백을 보유할 수 있는 만큼 가능합니다. 하지만 첫 번째 콜백이 콜백 중에 남은 시간을 모두 사용하면 다른 콜백에 더 이상 남은 시간이 없게 됩니다. 그러면 다른 콜백은 브라우저가 다음 유휴 상태가 될 때까지 기다려야 실행할 수 있습니다. 수행하려는 작업에 따라 단일 유휴 콜백을 사용하여 작업을 나누는 것이 더 나을 수 있습니다. 또는 시간 초과를 이용하여 콜백이 시간 부족으로 발생하지 않도록 할 수 있습니다.- 다른 유휴 콜백 내부에 새 유휴 콜백을 설정하면 어떻게 되나요? 새 유휴 콜백은 현재 프레임이 아닌 다음 프레임에서 시작하여 최대한 빨리 실행되도록 예약됩니다.
자리를 비우세요!
requestIdleCallback
는 사용자를 방해하지 않으면서 코드를 실행할 수 있는 멋진 방법입니다. 사용하기 쉽고 매우 유연합니다. 하지만 아직 초기 단계이고 사양이 완전히 확정되지는 않았으므로 언제든지 의견을 보내주시기 바랍니다.
Chrome Canary에서 확인하고, 프로젝트에 사용해 보고, 어떻게 진행되고 있는지 알려주세요.