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
을 기록합니다.childLane
가renderLanes
에 포함되지 않는지 확인합니다.- context 변경 후에도
childLane
이renderLanes
에 포함되지 않으면null
을 반환합니다. - context 변경이 필요없거나 현재
Fiber
가null
이면 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,
);
}