Early Riser
생각정리
Early Riser
전체 방문자
오늘
어제
  • 분류 전체보기 (128)
    • JS (19)
    • React (33)
    • React Native (2)
    • Library, Tool (13)
    • CSS (2)
    • Algorithm (40)
    • Computer Science (3)
    • 회고 (3)
    • AI (13)

블로그 메뉴

  • 홈
  • 태그
  • 방명록
  • 글쓰기

공지사항

인기 글

태그

  • 딥러닝
  • useEffect
  • 비동기
  • 파이썬
  • 논문리뷰
  • dfs
  • 부스트캠프 합격
  • boosting
  • 밑바닥
  • RNN
  • useState
  • react
  • 오늘의불경
  • 백준
  • BFS
  • 손실함수
  • 알고리즘
  • 프로그래머스
  • 완전탐색
  • 부스트캠프 합격 후기
  • lightgbm
  • 부스트캠프 9기
  • local minima
  • js
  • global minima
  • javascript
  • 구현
  • 자바스크립트
  • LGBM
  • 백트래킹

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Early Riser

생각정리

React

React 톺아보기 3. scheduler (1)

2025. 6. 29. 13:32

 reconciler에서 work를 microtask를 통해 schedule할때, scheduleCallback함수를 통해 performWorkOnRootViaSchedulerTask를 schedule하는 것을 보았다.

const newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performWorkOnRootViaSchedulerTask.bind(null, root),
    );

 react의 reconciler는 react element를 fiber로 확장시키는(mount, reconcile) 역할을 한다. 해당 fiber를 dom으로 이식하는 work를 schedule하는 것은 scheduler package가 하게 되는데, 이번 글에서는  scheduler에 대해서 살펴보자.

// packages/scheduler/src/forks/Scheduler.js

// Tasks are stored on a min heap
var taskQueue: Array<Task> = [];

// Incrementing id counter. Used to maintain insertion order.
var taskIdCounter = 1;

var currentTask = null;
var currentPriorityLevel = NormalPriority;

function unstable_scheduleCallback(
  priorityLevel: PriorityLevel,
  callback: Callback,
  options?: {delay: number},
): Task {
  var currentTime = getCurrentTime();

  var startTime;
  if (typeof options === 'object' && options !== null) {
	/*
    */
  } else {
    startTime = currentTime;
  }

  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      // Times out immediately
      timeout = -1;
      break;
    case UserBlockingPriority:
      // Eventually times out
      // userBlockingPriorityTimeout = 250;
      timeout = userBlockingPriorityTimeout;
      break;
    case IdlePriority:
      // Never times out
      // maxSigned31BitInt = 1073741823;
      timeout = maxSigned31BitInt;
      break;
    case LowPriority:
      // Eventually times out
      // lowPriorityTimeout = 10000;
      timeout = lowPriorityTimeout;
      break;
    case NormalPriority:
    default:
      // Eventually times out
      // const normalPriorityTimeout = 5000;
      timeout = normalPriorityTimeout;
      break;
  }

  var expirationTime = startTime + timeout;

  var newTask: Task = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };

  if (startTime > currentTime) {
	/*
    */
  } else {
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    // Schedule a host callback, if needed. If we're already performing work,
    // wait until the next time we yield.
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback();
    }
  }

  return newTask;
}

React의 Reconciler는 업데이트의 종류에 따라 Lane을 할당하고, 이를 통해 Scheduler의 우선순위를 결정한다. 

// packages/react-reconciler/src/ReactFiberRootScheduler.js

function scheduleTaskForRootDuringMicrotask(...){
    switch (lanesToEventPriority(nextLanes)) {
          // Scheduler does have an "ImmediatePriority", but now that we use
          // microtasks for sync work we no longer use that. Any sync work that
          // reaches this path is meant to be time sliced.
          case DiscreteEventPriority:
          case ContinuousEventPriority:
            schedulerPriorityLevel = UserBlockingSchedulerPriority;
            break;
          case DefaultEventPriority:
            schedulerPriorityLevel = NormalSchedulerPriority;
            break;
          case IdleEventPriority:
            schedulerPriorityLevel = IdleSchedulerPriority;
            break;
          default:
            schedulerPriorityLevel = NormalSchedulerPriority;
            break;
  	}
 }

 앞서 reconciler의 scheduleTaskForRootDuringMicrotask 함수에서는 lanesToEventPriority를 통해 Lane을 EventPriority로 변환한 후, 다시 Scheduler의 우선순위로 매핑하는 것을 보았다.이 우선순위 매핑은 다음과 같다:

  • DiscreteEventPriority/ContinuousEventPriority → UserBlockingSchedulerPriority (250ms)
  • DefaultEventPriority → NormalSchedulerPriority (5000ms)
  • IdleEventPriority → IdleSchedulerPriority (거의 무한대)

 Scheduler는 각 우선순위에 따라 서로 다른 timeout 값을 설정한다. 우선순위가 높을수록 timeout이 짧아지는 구조로, 이는 중요한 작업이 더 빨리 만료되어 우선적으로 처리되도록 보장한다.특히 IdleSchedulerPriority는 maxSigned31BitInt(약 10억)라는 매우 큰 값을 사용한다. 이는 Idle 작업이 브라우저가 유휴 상태일 때만 실행되어도 되는 작업이기 때문에, JavaScript에서 표현 가능한 가장 큰 숫자를 timeout으로 설정하여 사실상 만료되지 않도록 한 것이다.

 taskqueue는 minheap으로 구현되어 있으며, 정렬기준은 expirationTime이다.

//packages/scheduler/src/forks/Scheduler.js

function requestHostCallback() {
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
  // Node.js and old IE 환경.
  // setImmediate 사용.
} 
else if (typeof MessageChannel !== 'undefined') {
  // DOM and Worker environments.
  // We prefer MessageChannel because of the 4ms setTimeout clamping.
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  // non-browser 환경.
  // setTimeOut 사용.
}

 react를 사용하는 환경에 따라 macro task queue에 task를 예약하는 방법이 달라지지만, 모두 macro task queue를 활용하는 것을 볼 수 있다. macro task queue를 통해 performWorkUntilDeadline함수를 실행한다.

 즉, schedule 함수는 micro task queue를 통해 실행되고 실제 work를 수행하는performWorkOnRootViaSchedulerTask.bind(null, root), callback 함수, 는 macro task queue를 통해 실행된다.

 따라서 discrete event는 micro task의 flushSync를 통해 실행되고, inputContinous event 등은 macro task를 통해 실행되므로 discrete event의 우선순위가 보장됨을 알 수 있다.

//packages/scheduler/src/forks/Scheduler.js

const performWorkUntilDeadline = () => {
  if (enableRequestPaint) {
    needsPaint = false;
  }
  if (isMessageLoopRunning) {
    const currentTime = getCurrentTime();
    // Keep track of the start time so we can measure how long the main thread
    // has been blocked.
    startTime = currentTime;

    // If a scheduler task throws, exit the current browser task so the
    // error can be observed.
    //
    // Intentionally not using a try-catch, since that makes some debugging
    // techniques harder. Instead, if `flushWork` errors, then `hasMoreWork` will
    // remain true, and we'll continue the work loop.
    let hasMoreWork = true;
    try {
      hasMoreWork = flushWork(currentTime);
    } finally {
      if (hasMoreWork) {
        // If there's more work, schedule the next message event at the end
        // of the preceding one.
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
      }
    }
  }
};

이 함수는 macro task를 통해 실행되므로 start time을 계속해서 track하는 것을 볼 수 있다. 이는 메인 스레드를 오랫동안 점유하지 않기 위함이다. Concurrent render를 할때 메인 스레드 점유를 오래하면 브라우저이 렌더링이 block된다. 이는 frame rate를 지키지 못해 사용성을 떨어트릴 수 있으므로 work의 start time을 추적하여 설정된 시간을 넘긴 경우 workLoop에서 work를 중단할 수 있는 지표가 된다.

//packages/react-reconciler/src/ReactFiberWorkLoop.js

function workLoopConcurrentByScheduler() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
	performUnitOfWork(workInProgress);
  }
}

 스레드를 점유할 수 있는 시간인 frameYieldMs는 기본적으로 5이다. 물론 모니터의 주사율인 60hz를 맞추기 위해서는 16.6ms마다 paint를 해야 하지만, javascript 실행 후 critcal rendering path에 소요되는 시간까지 고려하여 16.6ms 보다 짧은 5ms로 설정한 것이다. 또한 다양한 환경에서 사용되는 react이기에 보수적으로 시간을 잡은것으로 보인다. 페이스북 웹사이트의 경우 10ms로 설정하여 더 높은 성능을 보인다 한다.
 performWorkUntilDeadline 함수는 재귀적으로 사용되는데, workloop에서 callback함수의 반환값이 함수라면 work가 남아잇다는 것이므로 schedulePerformWorkUntilDeadline를 다시 호출한다. schedulePerformWorkUntilDeadline는 macro task에 performWorkUntilDeadline를 예약하는 함수임을 기억하자. 즉, 이 재귀적 호출은 동기적이 아니라 비동기적으로 이루어진다.

이는 렌더링 작업을 중단 가능한 단위로 나누면서 브라우저가 중간에 다른 작업(이벤트 처리, 렌더링 등)을 수행할 기회를 얻는다.

하나의 큰 React 렌더링 작업이 여러 개의 작은 매크로태스크로 분할되어 실행되며, 각 태스크 사이사이에 브라우저가 다른 작업을 처리할 수 있어 전체적인 반응성이 유지된다. 

//packages/scheduler/src/forks/Scheduler.js

function flushWork(initialTime: number) {
  isHostCallbackScheduled = false;
  if (isHostTimeoutScheduled) {
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
    if (enableProfiling) {
       return workLoop(initialTime);
       /*
       성능 측정
       */
    } else {
      return workLoop(initialTime);
    }
  } finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
  }
}
//packages/scheduler/src/forks/Scheduler.js

function workLoop(initialTime: number) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    if (!enableAlwaysYieldScheduler) {
      if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
        // This currentTask hasn't expired, and we've reached the deadline.
        break;
      }
    }
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        // If a continuation is returned, immediately yield to the main thread
        // regardless of how much time is left in the current time slice.
        currentTask.callback = continuationCallback;
        advanceTimers(currentTime);
        return true;
      } else {
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
        advanceTimers(currentTime);
      }
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
    if (enableAlwaysYieldScheduler) {
      if (currentTask === null || currentTask.expirationTime > currentTime) {
        // This currentTask hasn't expired we yield to the browser task.
        break;
      }
    }
  }
  // Return whether there's additional work
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

callback함수를 실행하고 callback함수는 performWorkOnRootViaSchedulerTask.bind(null, root)이였다.

스케줄은 micro task를 통해 실행되므로 root를 미리 binding해놓고,

performWorkOnRootViaSchedulerTask(
  root: FiberRoot,
  didTimeout: boolean,
)

didTimeout은 didUserCallbackTimeout를 통해 workLoop에서 전달한다. 

'React' 카테고리의 다른 글

transition과 concurrent render  (0) 2025.12.20
React 톺아보기 4. reconciler (2)  (0) 2025.07.14
React 톺아보기 2. reconciler (1)  (0) 2025.06.09
React 톺아보기 - 1. ReactDom  (0) 2025.06.05
React에서 batch 처리와 렌더링 주기: 무한 재렌더링  (1) 2025.01.15
    'React' 카테고리의 다른 글
    • transition과 concurrent render
    • React 톺아보기 4. reconciler (2)
    • React 톺아보기 2. reconciler (1)
    • React 톺아보기 - 1. ReactDom
    Early Riser
    Early Riser
    2년차 프론트엔드 개발자입니다. https://github.com/EarlyRiser42

    티스토리툴바