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
  • 알고리즘
  • react
  • local minima
  • 부스트캠프 9기
  • LGBM
  • 파이썬
  • BFS
  • boosting
  • lightgbm
  • 딥러닝
  • 비동기
  • 손실함수
  • 오늘의불경
  • dfs
  • javascript
  • useState
  • 논문리뷰
  • 백트래킹
  • 부스트캠프 합격 후기
  • 부스트캠프 합격
  • 백준
  • global minima
  • RNN
  • js
  • 프로그래머스
  • 완전탐색

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Early Riser

생각정리

React

React 톺아보기 2. reconciler (1)

2025. 6. 9. 18:04

react dom에서 render를 호출하면 reconciler의 updateContainer 함수가 호출되는 것을 보았다. 

function updateContainerImpl(
  rootFiber: Fiber,
  lane: Lane,
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): void {

  /*
  */
  
  const update = createUpdate(lane);
  update.payload = {element};
  
  // 반환된 root는 FiberRootNode
  const root = enqueueUpdate(rootFiber, update, lane);
  if (root !== null) {
    startUpdateTimerByLane(lane);
    scheduleUpdateOnFiber(root, rootFiber, lane);
    entangleTransitions(root, rootFiber, lane);
  }
}

 updateContainer함수는 구현 함수를 다시 호출하는데 이 구현된 함수는 update를 concurrentQueue라는 전역변수에 fiber, queue, update, lane을 저장하고 lane을 fiber.lane에 합치는 역할을 한다. lane은 2진수로 lane을 합칠땐 or연산을 통해 합치는데, 이를 통해 fiber하나에서 여러 종류의 업데이트들이 발생하더라도 우선순위를 모두 기록할 수 있다. 

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

const concurrentQueues: Array<any> = [];
let concurrentQueuesIndex = 0;

function enqueueUpdate(
  fiber: Fiber,
  queue: ConcurrentQueue | null,
  update: ConcurrentUpdate | null,
  lane: Lane,
) {

  concurrentQueues[concurrentQueuesIndex++] = fiber;
  concurrentQueues[concurrentQueuesIndex++] = queue;
  concurrentQueues[concurrentQueuesIndex++] = update;
  concurrentQueues[concurrentQueuesIndex++] = lane;

  concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);

  fiber.lanes = mergeLanes(fiber.lanes, lane);
  const alternate = fiber.alternate;
  if (alternate !== null) {
    alternate.lanes = mergeLanes(alternate.lanes, lane);
  }
}

 

 그 후 scheduleUpdateOnFiber를 통해 스케줄러에게 업데이트가 발생햇다는 걸 알리는데, 이 스케줄러에서 렌더링작업을 위한 prepareFreshStack함수(새로운 workInProgress fiber를 만드는 함수)에서 finishQueueingConcurrentUpdate함수를 호출하면서 concurrentQueue에 담긴 업데이트를 fiber의 updateQueue에 연결하는 작업을 한다. 

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

// Describes where we are in the React execution stack
let executionContext: ExecutionContext = NoContext;
// The root we're working on
let workInProgressRoot: FiberRoot | null = null;
// The fiber we're working on
let workInProgress: Fiber | null = null;
// The lanes we're rendering
let workInProgressRootRenderLanes: Lanes = NoLanes;

let workInProgressSuspendedReason: SuspendedReason = NotSuspended;

export function scheduleUpdateOnFiber(
  root: FiberRoot,
  fiber: Fiber,
  lane: Lane,
) {
  if (
    // Suspended render phase
    (root === workInProgressRoot &&
      (workInProgressSuspendedReason === SuspendedOnData ||
        workInProgressSuspendedReason === SuspendedOnAction)) ||
    // Suspended commit phase
    root.cancelPendingCommit !== null
  ) {
    // The incoming update might unblock the current render. Interrupt the
    // current attempt and restart from the top.
    prepareFreshStack(root, NoLanes);
    const didAttemptEntireTree = false;
    markRootSuspended(
      root,
      workInProgressRootRenderLanes,
      workInProgressDeferredLane,
      didAttemptEntireTree,
    );
  }

  markRootUpdated(root, lane);

  if (
    (executionContext & RenderContext) !== NoContext &&
    root === workInProgressRoot
  ) {
    // This update was dispatched during the render phase. This is a mistake
    workInProgressRootRenderPhaseUpdatedLanes = mergeLanes(
      workInProgressRootRenderPhaseUpdatedLanes,
      lane,
    );
  } else {
    // This is a normal update, scheduled from outside the render phase. For
    // example, during an input event.
    /*
    성능 측정 함수들
    */

    if (root === workInProgressRoot) {
      // Received an update to a tree that's in the middle of rendering.
      if ((executionContext & RenderContext) === NoContext) {
        workInProgressRootInterleavedUpdatedLanes = mergeLanes(
          workInProgressRootInterleavedUpdatedLanes,
          lane,
        );
      }
      if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
        const didAttemptEntireTree = false;
        markRootSuspended(
          root,
          workInProgressRootRenderLanes,
          workInProgressDeferredLane,
          didAttemptEntireTree,
        );
      }
    }

    ensureRootIsScheduled(root);
  }
}

 먼저 첫번째 조건문이 의미하는 바를 먼저 살펴보자.

 조건문에서 root === workInProgressRoot를 먼저 체크한다. root를 확인하는 것은 마운트 중이 아님을 확인하려는 것이다. workInProgressRoot는 prepareFreshStack에서 root가 할당되기 때문이다.

 또한 workInProgressSuspendedReason이 SuspendedOnData 혹은 action과 같은지 확인한다.  현재 렌더링이 이미 Suspense로 인해 차단된 상태에서 새로운 업데이트가 들어왔을 때의 상황을 설명한다. 여기서 "unblock"은 새로운 업데이트가 기존에 차단된 렌더링을 해제할 수 있다는 의미이다. 예를 들어, 컴포넌트가 데이터 로딩으로 인해 Suspense 상태에 있을 때, 서버에서 데이터가 도착하거나 사용자가 다른 데이터를 요청하는 새로운 업데이트가 발생할 수 있다. 이런 경우 새로운 업데이트는 이전에 차단되었던 렌더링을 무의미하게 만들거나 필요한 데이터를 제공하여 차단을 해제할 수 있다.

 또한 root.cancelPendingCommit !== null을 확인하여 커밋 단계가 중단된 상태를 체크한다. 이는 View Transition이나 Suspensey 커밋으로 인해 커밋이 지연되고 있는 상황을 의미한다.이 조건문들이 의미하는 바는 현재 React가 데이터 로딩이나 비동기 작업으로 인해 렌더링 또는 커밋 단계에서 대기 상태에 있다는 것이다. 이런 상황 역시 새로운 업데이트가 들어오면, 해당 업데이트가 기존에 중단된 작업을 차단 해제할 수 있는 정보를 포함할 가능성이 있다. 예를 들어, 사용자가 다른 데이터를 요청하는 버튼을 클릭하거나, 이전에 로딩 중이던 데이터가 도착했을 수 있다.따라서 React는 기존의 중단된 렌더링을 완전히 버리고 새로운 업데이트로 처음부터 다시 시작한다. 

 조건문에 해당되면 먼저 prepareFreshStack에서는 workInProgress를 createWorkInProgress(root.current, null)을 통해 새로운 root fiber로 할당하고, 모든 전역 변수들을 초기화하여 root fiber부터 다시 시작한다 (restart from the top(=rootfiber)). 그리고 finishQueueingConcurrentUpdates()를 호출하여 concurrent queues를 초기화한다. 이어서 markRootSuspended에서는 root.suspendedLanes |= suspendedLanes를 통해 suspended lanes를 root에 추가하고, root.pingedLanes &= ~suspendedLanes를 통해 pinged lanes에서 해당 lanes를 제거한다. 마지막으로 suspended된 작업들은 더 이상 CPU bound task가 아니기 때문에 (IO bound task) expirationTimes[index] = NoTimestamp를 통해 만료 시간을 제거한다.

markRootUpdated 함수는  OR 연산을 통해 새로운 업데이트의 lane을 root.pendingLanes에 추가한다.

 

그다음 살펴볼 함수는 ensureRootIsScheduled이다. 

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

let didScheduleMicrotask: boolean = false;

let mightHavePendingSyncWork: boolean = false;

export function ensureRootIsScheduled(root: FiberRoot): void {
  // This function is called whenever a root receives an update. 
  // It does two  things 
  // 1) it ensures the root is in the root schedule, and 
  // 2) it ensures there's a pending microtask to process the root schedule.

  // Add the root to the schedule
  if (root === lastScheduledRoot || root.next !== null) {
  } else {
    if (lastScheduledRoot === null) {
      firstScheduledRoot = lastScheduledRoot = root;
    } else {
      lastScheduledRoot.next = root;
      lastScheduledRoot = root;
    }
  }

  mightHavePendingSyncWork = true;
  
  if (!didScheduleMicrotask) {
    didScheduleMicrotask = true;
    scheduleImmediateRootScheduleTask();
  }
}

 ensureRootIsScheduled는 두가지 작업을 한다.

1. root가 scehduled 되엇나 확인하고 아니라면 순환 참조로 root를 scehdule 변수에 할당한다. 

2. scheduleImmediateRootScheduleTask()를 통해 work를 microtask로 schedule한다.

function scheduleImmediateRootScheduleTask() {
  if (supportsMicrotasks) {
    scheduleMicrotask(() => {
      processRootScheduleInMicrotask();
    });
  } else {
    Scheduler_scheduleCallback(
      ImmediateSchedulerPriority,
      processRootScheduleInImmediateTask,
    );
  }
}

processRootScheduleInMicrotask 함수를 microtaskqueue에 스케줄하는것을 볼 수 있다. 

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

function processRootScheduleInMicrotask() {
  didScheduleMicrotask = false;

  mightHavePendingSyncWork = false;

  let syncTransitionLanes = NoLanes;
  if (currentEventTransitionLane !== NoLane) {
    if (shouldAttemptEagerTransition()) {
      syncTransitionLanes = currentEventTransitionLane;
    }
    currentEventTransitionLane = NoLane;
  }

  const currentTime = now();

  let prev = null;
  let root = firstScheduledRoot;
  while (root !== null) {
    const next = root.next;
    const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
    if (nextLanes === NoLane) {
      // This root has no more pending work. Remove it from the schedule.
      // Null this out so we know it's been removed from the schedule.
      root.next = null;
      if (prev === null) {
        firstScheduledRoot = next;
      } else {
        prev.next = next;
      }
      if (next === null) {
        lastScheduledRoot = prev;
      }
    } else {
      // This root still has work. Keep it in the list.
      prev = root;

      if (
        syncTransitionLanes !== NoLanes ||
        includesSyncLane(nextLanes)
      ) {
        mightHavePendingSyncWork = true;
      }
    }
    root = next;
  }

  // At the end of the microtask, flush any pending synchronous work.
  flushSyncWorkAcrossRoots_impl(syncTransitionLanes, false);
}

 React에서 여러 root가 존재하는 경우는 createRoot를 여러 번 호출한 경우다. 각각이 별도의 React 앱이 되어 단방향 원형 연결 리스트로 관리된다. processRootScheduleInMicrotask에서는 모든 root를 순회하면서 각 root의 작업을 스케줄링한다.

 중요한 점은 scheduleTaskForRootDuringMicrotask에서 우선순위가 가장 높은 그룹 하나만 선택한다는 것이다. 또한 linked list를 순회하면서 처리한다는 점이다.

 같은 우선순위 그룹 내에서는 여러 lane을 함께 반환할 수 있지만, 서로 다른 우선순위 간에는 높은 우선순위만 선택된다. 예를 들어 root에 DefaultLane과 TransitionLane들이 있다면 DefaultLane만 먼저 처리되고, TransitionLane들은 다른 root를 모두 순회한 다음, 다음 스케줄링 사이클에서 처리된다.이렇게 함으로써 모든 root가 공평하게 높은 우선순위 작업을 먼저 처리할 기회를 얻는다. root1의 낮은 우선순위 작업이 root2의 높은 우선순위 작업을 블록하지 않으므로 사용자 상호작용이 다른 앱의 무거운 작업에 방해받지 않는다.

이러한 과정은 while 루프를 통해 모든 Root를 순회하면서 진행된다. 각 Root를 방문할 때마다 nextLanes를 확인하고, NoLane인 경우에는 해당 Root를 스케줄링 목록에서 제거한다. 이는 root.next를 null로 설정하고, 연결 리스트의 구조를 조정하여 이루어진다.

 주목할 점은 microtask의 마지막에 flushSyncWorkAcrossRoots가 호출된다는 점이다. react에서 sync work란 legacy root혹은 discrete event에 의해 생성된 update를 의미한다. 물론 inputcontinous lane부터 defaultlane도 sync render를 거치긴 하지만, discrete event에 의해 생성된 update는 scheduler를 거치지 않고 render 됨을 볼 수 있다. flushSyncWorkAcrossRoots도 scehduler와 마찬가지로 performWorkOnRoot를 호출하게 되는 것은 마찬가지이므로 분석에서 제외한다.

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

function scheduleTaskForRootDuringMicrotask(
  root: FiberRoot,
  currentTime: number,
): Lane {
  // This function is always called inside a microtask, or at the very end of a
  // rendering task right before we yield to the main thread. It should never be
  // called synchronously.

  // This function also never performs React work synchronously; it should
  // only schedule work to be performed later, in a separate task or microtask.

  markStarvedLanesAsExpired(root, currentTime);

  // Determine the next lanes to work on, and their priority.
  const rootWithPendingPassiveEffects = getRootWithPendingPassiveEffects();
  const pendingPassiveEffectsLanes = getPendingPassiveEffectsLanes();
  const workInProgressRoot = getWorkInProgressRoot();
  const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
  const rootHasPendingCommit =
    root.cancelPendingCommit !== null || root.timeoutHandle !== noTimeout;
  const nextLanes =
    enableYieldingBeforePassive && root === rootWithPendingPassiveEffects
      ? pendingPassiveEffectsLanes
      : getNextLanes(
          root,
          root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
          rootHasPendingCommit,
        );

  const existingCallbackNode = root.callbackNode;
  if (
    // Check if there's nothing to work on
    nextLanes === NoLanes ||
    // If this root is currently suspended and waiting for data to resolve, don't
    // schedule a task to render it. We'll either wait for a ping, or wait to
    // receive an update.
    //
    // Suspended render phase
    (root === workInProgressRoot && isWorkLoopSuspendedOnData()) ||
    // Suspended commit phase
    root.cancelPendingCommit !== null
  ) {
    // Fast path: There's nothing to work on.
    if (existingCallbackNode !== null) {
      cancelCallback(existingCallbackNode);
    }
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return NoLane;
  }

  // Schedule a new callback in the host environment.
  if (
    includesSyncLane(nextLanes) &&
    // If we're prerendering, then we should use the concurrent work loop
    // even if the lanes are synchronous, so that prerendering never blocks
    // the main thread.
    !(enableSiblingPrerendering && checkIfRootIsPrerendering(root, nextLanes))
  ) {
    // Synchronous work is always flushed at the end of the microtask, so we
    // don't need to schedule an additional task.
    if (existingCallbackNode !== null) {
      cancelCallback(existingCallbackNode);
    }
    root.callbackPriority = SyncLane;
    root.callbackNode = null;
    return SyncLane;
  } else {
    // We use the highest priority lane to represent the priority of the callback.
    const existingCallbackPriority = root.callbackPriority;
    const newCallbackPriority = getHighestPriorityLane(nextLanes);

    if (
      newCallbackPriority === existingCallbackPriority &&
      // Special case related to `act`. If the currently scheduled task is a
      // Scheduler task, rather than an `act` task, cancel it and re-schedule
      // on the `act` queue.
      !(
        __DEV__ &&
        ReactSharedInternals.actQueue !== null &&
        existingCallbackNode !== fakeActCallbackNode
      )
    ) {
      // The priority hasn't changed. We can reuse the existing task.
      return newCallbackPriority;
    } else {
      // Cancel the existing callback. We'll schedule a new one below.
      cancelCallback(existingCallbackNode);
    }

    let schedulerPriorityLevel;
    switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority:
      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingSchedulerPriority;
        break;
      case DefaultEventPriority:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
      case IdleEventPriority:
        schedulerPriorityLevel = IdleSchedulerPriority;
        break;
      default:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
    }

    // scheduleCallback의 return값은 task 객체이다. task 객체는 taskqueue에 들어가잇음.
    const newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performWorkOnRootViaSchedulerTask.bind(null, root),
    );

    root.callbackPriority = newCallbackPriority;
    root.callbackNode = newCallbackNode;
    return newCallbackPriority;
  }
}

스케줄링은 firstScheduledRoot부터 시작된다. 이 Root를 시작으로 각 Root의 업데이트를 순차적으로 처리하는데, 이 과정에서 각 Root의 highest lane을 추출하여 우선순위를 결정한다. 만약 nextLane이 NoLane이거나 sync lane이라면, 기존의 callbackNode를 취소하고 callbackPriority를 nextLane으로 설정한다.반면, discrete나 input continuous와 같은 다른 우선순위의 lane이 있다면, 이는 새로운 task로 등록된다. 이 task는 unstable_scheduleCallback을 통해 taskQueue에 등록된다. 

반환된 task 객체는 root fiber에 저장되는데, 이는 task가 우선순위에서 밀리거나 suspense등에 의해 취소될수 있기 때문이다. task queue는 min heap구조로, 원하는 task를 heap에서 뺄 수 없기에(O(n)의 시간복잡도) task를 취소해야 할 경우, root의 callbacknode의 callback을 null로 바꾼다. 이때 callbacknode는 task queue의 task의 참조포인터이므로 task queue의 task callback도 null이 됨을 알 수 있다. 이를 통해 scheduler의 workLoop에서 취소된 task를 skip할 수 있다.

ㅇ 앞서 processRootScheduleInMicrotask에서 root를 순회하며 task를 예약하는 것을 보았다. scehdule된 callback은 scheduler의 requestHostCallback을 통해 macrotask queue에 등록되어 실행된다. 이제 performWorkOnRootViaSchedulerTask.bind(null, root)를 하는 이유를 알았다. callback은 macro task queue를 통해 실행되므로 변수들을 bind해야할 필요가 있다. 또한 performWorkOnRootViaSchedulerTask는 reconcilation->commit을 통해 paint까지를 처리한다. 즉, 작업의 우선순위에 따라 paint까지 완료한 후 다시 work에 들어가는 것을 알 수 있다.

 getNextLanes 함수는 root의 pendingLanes, suspendedLanes, pingedLanes, warmLanes 를 고려하여 다음에 처리할 업데이트의 우선순위를 결정한다.

먼저 pendingLanes가 NoLanes인 경우, 즉 처리할 작업이 없는 경우에는 즉시 NoLanes를 반환한다. 그렇지 않은 경우, nonIdlePendingLanes를 확인하여 우선순위가 높은 작업이 있는지 검사한다. nonIdlePendingLanes는 NonIdleLanes와 pendingLanes의 비트 연산으로 구해지며, 이는 Idle 우선순위가 아닌 모든 작업을 포함한다.nonIdlePendingLanes가 존재하는 경우, 먼저 suspendedLanes와 겹치지 않는 nonIdleUnblockedLanes를 확인한다. 이는 현재 중단되지 않은 작업들을 의미한다. 만약 이러한 작업이 있다면, getHighestPriorityLanes를 통해 가장 높은 우선순위의 작업을 선택한다.

만약 중단되지 않은 작업이 없다면, pingedLanes를 확인한다. pingedLanes는 이전에 중단되었다가 다시 시작할 준비가 된 작업들을 나타낸다. 이러한 작업이 있다면 마찬가지로 가장 높은 우선순위의 작업을 선택한다.enableSiblingPrerendering이 활성화되어 있고, rootHasPendingCommit이 false인 경우, 아직 예열되지 않은 작업들(lanesToPrewarm)을 확인하여 처리할 수 있다.

이는 성능 최적화를 위한 기능이다.nonIdlePendingLanes가 없는 경우, 즉 남은 작업이 모두 Idle 우선순위인 경우에도 비슷한 로직을 적용하되, Idle 작업에 대해서도 동일한 우선순위 결정 과정을 거친다.

마지막으로, 이미 진행 중인 렌더링(wipLanes)이 있는 경우, 새로운 작업의 우선순위가 현재 진행 중인 작업보다 높은지 확인한다. 만약 새로운 작업의 우선순위가 더 낮거나 같다면, 현재 진행 중인 작업을 계속한다. 이는 불필요한 렌더링 중단을 방지하기 위한 것이다.

'React' 카테고리의 다른 글

React 톺아보기 4. reconciler (2)  (0) 2025.07.14
React 톺아보기 3. scheduler (1)  (0) 2025.06.29
React 톺아보기 - 1. ReactDom  (0) 2025.06.05
React에서 batch 처리와 렌더링 주기: 무한 재렌더링  (1) 2025.01.15
React에서 경로 최적화를 통한 성능 향상  (0) 2025.01.14
    'React' 카테고리의 다른 글
    • React 톺아보기 4. reconciler (2)
    • React 톺아보기 3. scheduler (1)
    • React 톺아보기 - 1. ReactDom
    • React에서 batch 처리와 렌더링 주기: 무한 재렌더링
    Early Riser
    Early Riser
    2년차 프론트엔드 개발자입니다. https://github.com/EarlyRiser42

    티스토리툴바