React-reconciler | Lane (19.0.0)

작업들이 어떤 우선순위 도로(Lane)에 있는지 확인하고 이미 작업된 목록은 제거하여 불필요한 렌더링을 방지합니다.

Lane

react/packages/react-reconciler/src/ReactFiberLane.js
export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;
 
export const SyncHydrationLane: Lane = /*               */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000010;
export const SyncLaneIndex: number = 1;
 
export const InputContinuousHydrationLane: Lane = /*    */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000001000;
 
export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /*                     */ 0b0000000000000000000000000100000;
 
export const SyncUpdateLanes: Lane = SyncLane | InputContinuousLane | DefaultLane;
 
const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000000000001000000;
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111111111110000000;
const TransitionLane1: Lane = /*                        */ 0b0000000000000000000000010000000;
const TransitionLane2: Lane = /*                        */ 0b0000000000000000000000100000000;
const TransitionLane3: Lane = /*                        */ 0b0000000000000000000001000000000;
const TransitionLane4: Lane = /*                        */ 0b0000000000000000000010000000000;
const TransitionLane5: Lane = /*                        */ 0b0000000000000000000100000000000;
const TransitionLane6: Lane = /*                        */ 0b0000000000000000001000000000000;
const TransitionLane7: Lane = /*                        */ 0b0000000000000000010000000000000;
const TransitionLane8: Lane = /*                        */ 0b0000000000000000100000000000000;
const TransitionLane9: Lane = /*                        */ 0b0000000000000001000000000000000;
const TransitionLane10: Lane = /*                       */ 0b0000000000000010000000000000000;
const TransitionLane11: Lane = /*                       */ 0b0000000000000100000000000000000;
const TransitionLane12: Lane = /*                       */ 0b0000000000001000000000000000000;
const TransitionLane13: Lane = /*                       */ 0b0000000000010000000000000000000;
const TransitionLane14: Lane = /*                       */ 0b0000000000100000000000000000000;
const TransitionLane15: Lane = /*                       */ 0b0000000001000000000000000000000;
 
const RetryLanes: Lanes = /*                            */ 0b0000011110000000000000000000000;
const RetryLane1: Lane = /*                             */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /*                             */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /*                             */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /*                             */ 0b0000010000000000000000000000000;
 
export const SomeRetryLane: Lane = RetryLane1;
 
export const SelectiveHydrationLane: Lane = /*          */ 0b0000100000000000000000000000000;
 
const NonIdleLanes: Lanes = /*                          */ 0b0000111111111111111111111111111;
 
export const IdleHydrationLane: Lane = /*               */ 0b0001000000000000000000000000000;
export const IdleLane: Lane = /*                        */ 0b0010000000000000000000000000000;
 
export const OffscreenLane: Lane = /*                   */ 0b0100000000000000000000000000000;
export const DeferredLane: Lane = /*                    */ 0b1000000000000000000000000000000;
 
 
export const UpdateLanes: Lanes = SyncLane | InputContinuousLane | DefaultLane | TransitionLanes;
 
export const HydrationLanes =
  SyncHydrationLane |
  InputContinuousHydrationLane |
  DefaultHydrationLane |
  TransitionHydrationLane |
  SelectiveHydrationLane |
  IdleHydrationLane;

NoLane

  • 작업이 없는 상태를 나타냅니다.

SyncLane

  • 가장 높은 우선순위를 가지며 사용자 상호작용(클릭, 입력 등)을 즉각 처리해야 할 때 사용됩니다. 해당 Lane에 할당된 작업은 이름과 같이 동기적으로 렌더링됩니다. 이유는 높은 UI 반응성을 위해서입니다.

SyncHydrationLane

  • 서버에서 전송된 HTML과 React의 상태를 동기화할 때 사용됩니다.

InputContinuousLane

  • 두 번째 우선순위를 가지며 지속적인 입력 이벤트(예: 키 입력, 스크롤)와 관련된 작업입니다.

InputContinuousHydrationLane

  • 입력 이벤트(예: 키 입력, 스크롤)와 관련된 서버-클라이언트 동기화를 처리합니다.

DefaultLane

  • 세 번째 우선순위를 가지며 리액트 외부(fetch, setTimeout 등)에서 발생한 이벤트를 처리합니다.

TransitionLane

  • 15개의 TransitionLane(1~15)으로 세분화되어 있으며 useTransition, startTransition을 사용하여 지연된 작업입니다.

RetryLane

  • 4개의 RetryLane(1~4)으로 세분화되어 있으며 실패한 작업을 재시도할 때 사용됩니다.

IdleLane

  • 네 번째 우선순위를 가지며 매우 낮은 우선순위의 작업으로 CPU가 유휴 상태일 때만 처리됩니다.

OffscreenLane

  • 화면 밖에서 발생하는 작업(숨겨진 콘텐츠의 준비 등)입니다.

DeferredLane

  • useDeferredValue를 사용하여 지연된 작업입니다.

Lane 우선순위

react/packages/react-reconciler/src/ReactEventPriorities.js
export const DiscreteEventPriority: EventPriority = SyncLane;
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
export const DefaultEventPriority: EventPriority = DefaultLane;
export const IdleEventPriority: EventPriority = IdleLane;

사용자 상호작용 -> 마우스 드래그와 같은 지속적인 이벤트 -> 비동기 요청 및 상태 업데이트 -> 유휴 상태 순으로 우선순위를 갖습니다.

SyncLane, InputContinuousLane event 분류

react/packages/react-dom-bindings/src/events/ReactDOMEventlistener.js
export function getEventPriority(domEventName: DOMEventName): EventPriority {
  switch (domEventName) {
    // Used by SimpleEventPlugin:
    case 'beforetoggle':
    case 'cancel':
    case 'click':
    case 'close':
    case 'contextmenu':
    case 'copy':
    case 'cut':
    case 'auxclick':
    case 'dblclick':
    case 'dragend':
    case 'dragstart':
    case 'drop':
    case 'focusin':
    case 'focusout':
    case 'input':
    case 'invalid':
    case 'keydown':
    case 'keypress':
    case 'keyup':
    case 'mousedown':
    case 'mouseup':
    case 'paste':
    case 'pause':
    case 'play':
    case 'pointercancel':
    case 'pointerdown':
    case 'pointerup':
    case 'ratechange':
    case 'reset':
    case 'resize':
    case 'seeked':
    case 'submit':
    case 'toggle':
    case 'touchcancel':
    case 'touchend':
    case 'touchstart':
    case 'volumechange':
    // Used by polyfills: (fall through)
    case 'change':
    case 'selectionchange':
    case 'textInput':
    case 'compositionstart':
    case 'compositionend':
    case 'compositionupdate':
    // Only enableCreateEventHandleAPI: (fall through)
    case 'beforeblur':
    case 'afterblur':
    // Not used by React but could be by user code: (fall through)
    case 'beforeinput':
    case 'blur':
    case 'fullscreenchange':
    case 'focus':
    case 'hashchange':
    case 'popstate':
    case 'select':
    case 'selectstart':
      return DiscreteEventPriority;
    case 'drag':
    case 'dragenter':
    case 'dragexit':
    case 'dragleave':
    case 'dragover':
    case 'mousemove':
    case 'mouseout':
    case 'mouseover':
    case 'pointermove':
    case 'pointerout':
    case 'pointerover':
    case 'scroll':
    case 'touchmove':
    case 'wheel':
    // Not used by React but could be by user code: (fall through)
    case 'mouseenter':
    case 'mouseleave':
    case 'pointerenter':
    case 'pointerleave':
      return ContinuousEventPriority;
    ...
    default:
      return DefaultEventPriority;
  }
}

다음과 같이 지속적인 이벤트냐 아니냐에 따라 SyncLane, InputContinuousLane가 분류됩니다.

Lane 위 하나의 Fiber 재조정 작업을 마친 후

react/packages/scheduler/src/SchedulerFeatureFlag.js
export const frameYieldMs = 5;
react/packages/scheduler/src/forks/Scheduler.js
let frameInterval = frameYieldMs;
 
function shouldYieldToHost(): boolean {
  const timeElapsed = getCurrentTime() - startTime;
  if (timeElapsed < frameInterval) {
    // The main thread has only been blocked for a really short amount of time;
    // smaller than a single frame. Don't yield yet.
    return false;
  }
  // Yield now.
  return true;
}

Lane 위 (실제로 올라가진 않지만 직역합니다.) 1개 Fiber의 재조정 작업을 마치고 나면 다음과 같이 작업 시작 시간과 5ms 이상 차이나면 메인 스레드에 양보합니다. 60fps (16.67ms)에서 끊김없이 렌더링하기 위함입니다.

react/packages/react-reconciler/src/ReactFiberWorkLoop.js
export let entangledRenderLanes: Lanes = NoLanes;
 
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
 
function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;
 
  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    if (__DEV__) {
      ...
    } else {
      next = beginWork(current, unitOfWork, entangledRenderLanes);
    }
    stopProfilerTimerIfRunningAndRecordDuration(unitOfWork);
  } else {
    if (__DEV__) {
      ...
    } else {
      next = beginWork(current, unitOfWork, entangledRenderLanes);
    }
  }
  ...
}

workLoopConcurrent 메소드에서 workInProgress 트리를 순회할 때 항상 양보해야 하는지 체크합니다.

어떻게 불필요한 렌더링을 방지하나요?

react/packages/react-reconciler/src/ReactFiberBeginWork.js
function updateFunctionComponent(
  current: null | Fiber,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {
  ...
  if (current !== null && !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderLanes);
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
  ...
}

다음과 같이 컴포넌트를 업데이트할 때 didReceiveUpdate을 기반으로 렌더링을 건너뜁니다.

react/packages/react-reconciler/src/ReactFiberBeginWork.js
function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  ...
  markSkippedUpdateLanes(workInProgress.lanes);
 
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    if (enableLazyContextPropagation && current !== null) {
      lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
      if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
        return null;
      }
    } else {
      return null;
    }
  }
 
  cloneChildFibers(current, workInProgress);
  return workInProgress.child;
}
  • markSkippedUpdateLanes 메소드를 통해 건너뛴 Lane을 기록합니다.
  • childLanerenderLanes에 포함되지 않는지 확인합니다.
  • context 변경 후에도 childLanerenderLanes에 포함되지 않으면 null을 반환합니다.
  • context 변경이 필요없거나 현재 Fibernull이면 null을 리턴합니다.
  • 현재 Fiber에 작업은 없지만 자식 Fiber에는 작업이 남아 있는 경우는 작업을 이어갑니다.
react/packages/react-reconciler/src/ReactFiberWorkLoop.js
let workInProgressRootSkippedLanes: Lanes = NoLanes;
...
export function markSkippedUpdateLanes(lane: Lane | Lanes): void {
  workInProgressRootSkippedLanes = mergeLanes(
    lane,
    workInProgressRootSkippedLanes,
  );
}

참고 자료

goidle | React 18 톺아보기 - 02.Lane 모델