react fiber 到底有多細

圖片

作者:堅果

部門:業務技術 / 前端

前言

=====

Fiber 是對 React 核心算法的重構,facebook 團隊使用兩年多的時間去重構 React 的核心算法,在 React16 以上的版本中引入了 Fiber 架構,極大的提高了大型 react 項目的性能,也激發了我對其實現的好奇。在研究源碼的過程中,能發現很多比較細的點,有任務單元拆分的細,有任務調度、雙緩衝、節點複用等優化的細,都非常值得我們學習,接下來就帶大家看看 react fiber 到底有多細。

一、我們爲什麼需要 react fiber

react 在進行組件渲染時,從 setState 開始到渲染完成整個過程是同步的(“一氣呵成”)。如果需要渲染的組件比較龐大,js 執行會佔據主線程時間較長,會導致頁面響應度變差,使得動畫、手勢交互等事件產生卡頓。

爲了解決這個問題,React 提供 pureComponent,shouldComponentUpdate,useMemo,useCallback 讓開發者來操心哪些 subtree 是需要重新渲染的,哪些是不需要重新渲染的。究其本質,是因爲 React 採用 jsx 語法過於靈活,不理解開發者寫出代碼所代表的意義,沒有辦法做出優化。

爲什麼 JS 長時間執行會影響交互響應、動畫?因爲 JavaScript 在瀏覽器的主線程上運行,恰好與樣式計算、佈局以及許多情況下的繪製一起運行。如果 JavaScript 運行時間過長,就會阻塞這些其他工作,可能導致掉幀。

因此,爲了解決以上的痛點問題,React 希望能夠徹底解決主線程長時間佔用問題,於是引入了 Fiber 來改變這種不可控的現狀,把渲染 / 更新過程拆分爲一個個小塊的任務,通過合理的調度機制來調控時間,指定任務執行的時機,從而降低頁面卡頓的概率,提升頁面交互體驗。通過 Fiber 架構,讓 reconcilation 過程變得可被中斷。適時地讓出 CPU 執行權,可以讓瀏覽器及時地響應用戶的交互。

由此 react fiber 的任務就很清晰了

  1. 把渲染 / 更新過程拆分爲更小的、可中斷的工作單元

  2. 在瀏覽器空閒時執行工作循環

  3. 將所有執行結果彙總 patch 到真實 DOM 上

二、工作單元

如何拆分工作,這是最基礎也是最重要的工作。

2.1 拆什麼,什麼不能拆?

把渲染 / 更新過程分爲 2 個階段(diff + patch):

1.diff ~ render/reconciliation
2.patch ~ commit

diff 的實際工作是對比 prevInstance 和 nextInstance 的狀態,找出差異及其對應的 DOM change。diff 本質上是一些計算(遍歷、比較),是可拆分的(算一半待會兒接着算) patch 階段把本次更新中的所有 DOM change 應用到 DOM 樹,是一連串的 DOM 操作。這些 DOM 操作雖然看起來也可以拆分(按照 change list 一段一段做),但這樣做一方面可能造成 DOM 實際狀態與維護的內部狀態不一致,另外還會影響體驗。而且,一般場景下,DOM 更新的耗時比起 diff 及生命週期函數耗時不算什麼,拆分的意義不很大

所以,render/reconciliation 階段的工作(diff)可以拆分,commit 階段的工作(patch)不可拆分

2.2 怎麼拆?

先憑空亂來幾種 diff 工作拆分方案:

  1. 按組件結構拆。不好分,無法預估各組件更新的工作量

  2. 按實際工序拆。比如分爲 getNextState(), shouldUpdate(), updateState(), checkChildren() 再穿插一些生命週期函數

按組件拆太粗,顯然對大組件不太公平。按工序拆太細,任務太多,頻繁調度不划算。那麼有沒有合適的拆分單位?

2.3 Fiber

有。react 的拆分單位是 fiber(fiber tree 上的一個節點),實際上就是按虛擬 DOM 節點拆,因爲 fiber tree 是根據 vDOM tree 構造出來的,樹結構一模一樣,只是節點攜帶的信息有差異。

fiber tree 上各節點的主要結構如下:

// fiber tree節點結構
{
    // The local state associated with this fiber.
    stateNode,
    // Singly Linked List Tree Structure.
    child,
    return,
    sibling,
    // Effect
      effectTag,
    // Singly linked list fast path to the next fiber with side-effects.
    nextEffect,
    // The first and last fiber with side-effect within this subtree. This allows
    // us to reuse a slice of the linked list when we reuse the work done within
    // this fiber.
    firstEffect,
    lastEffect,
    ...
}

其中的 child(第一個子節點)、sibling(兄弟節點)、return(父節點)等屬性,形成了如下的鏈表樹結構: 

而 effectTag、nextEffect、firstEffect、lastEffect 爲 effect 相關信息,保存當前 diff 的成果。這些參數共同爲後續的工作循環提供了可能,使 react 可以在執行完每個 fiber 時停下,根據瀏覽器的繁忙情況判斷是否繼續往下執行,因此我們也可以將 fiber 理解成一個工作單元

至此,react fiber 已經準備好了異步渲染的前置工作,接下來看看瀏覽器爲其提供了哪些助攻。

三、瀏覽器能力

介紹瀏覽器能力之前,我們先了解下瀏覽器渲染的基礎知識。

3.1 渲染幀

我們知道,在瀏覽器中,頁面是一幀一幀繪製出來的,渲染的幀率與設備的刷新率保持一致。一般情況下,設備的屏幕刷新率爲 1s 60 次,當每秒內繪製的幀數(FPS)超過 60 時,頁面渲染是流暢的;而當 FPS 小於 60 時,會出現一定程度的卡頓現象。下面來看完整的一幀中,具體做了哪些事情

  1. 首先需要處理輸入事件,能夠讓用戶得到最早的反饋

  2. 接下來是處理定時器,需要檢查定時器是否到時間,並執行對應的回調

  3. 接下來處理 Begin Frame(開始幀),即每一幀的事件,包括 window.resize、scroll、media query change 等

  4. 接下來執行請求動畫幀 requestAnimationFrame(rAF),即在每次繪製之前,會執行 rAF 回調

  5. 緊接着進行 Layout 操作,包括計算佈局和更新佈局,即這個元素的樣式是怎樣的,它應該在頁面如何展示

  6. 接着進行 Paint 操作,得到樹中每個節點的尺寸與位置等信息,瀏覽器針對每個元素進行內容填充

到這時以上的六個階段都已經完成了,接下來處於空閒階段(Idle Peroid),可以在這時執行 requestIdleCallback 裏註冊的任務(它就是 React Fiber 任務調度實現的基礎)

3.2 RequestIdleCallback

RequestIdleCallback 是 react Fiber 實現的基礎 api 。該方法將在瀏覽器的空閒時段內調用的函數排隊,使開發者在主事件循環上執行後臺和低優先級的工作,而不影響延遲關鍵事件,如動畫和輸入響應。正常幀任務完成後沒超過 16ms,說明有多餘的空閒時間,此時就會執行 requestIdleCallback 裏註冊的任務。

可以參考下圖來理解 requestIdleCallback 在每幀中的調用

  1. 低優先級任務由 requestIdleCallback 處理;

  2. 高優先級任務,如動畫相關的由 requestAnimationFrame 處理;

  3. requestIdleCallback 可以在多個空閒期調用空閒期回調,執行任務;

window.requestIdleCallback(callback) 的 callback 中會接收到默認參數 deadline ,其中包含了以下兩個屬性:

requestIdleCallback 方法非常重要,下面分別講兩個例子來理解這個方法,在每個例子中都需要執行多個任務,但是任務的執行時間是不一樣的,下面來看瀏覽器是如何分配時間執行這些任務的:

一幀執行

直接執行 task1、task2、task3,各任務的時間總和小於 16ms:

const sleep = (delay) => {
  const start = Date.now();
  while (Date.now() - start <= delay) {}
};
const taskQueue = [
  () => {
    console.log("task1 start");
    sleep(3);
    console.log("task1 end");
  },
  () => {
    console.log("task2 start");
    sleep(3);
    console.log("task2 end");
  },
  () => {
    console.log("task3 start");
    sleep(3);
    console.log("task3 end");
  },
];
const performUnitWork = () => {
  // 取出第一個隊列中的第一個任務並執行
  taskQueue.shift()();
};
const workloop = (deadline) => {
  console.log(`此幀的剩餘時間爲: ${deadline.timeRemaining()}`);
  // 如果此幀剩餘時間大於0或者已經到了定義的超時時間(上文定義了timeout時間爲1000,到達時間時必須強制執行),且當時存在任務,則直接執行這個任務
  // 如果沒有剩餘時間,則應該放棄執行任務控制權,把執行權交還給瀏覽器
  while (
    (deadline.timeRemaining() > 0 || deadline.didTimeout) &&
    taskQueue.length > 0
  ) {
    performUnitWork();
  }
  // 如果還有未完成的任務,繼續調用requestIdleCallback申請下一個時間片
  if (taskQueue.length > 0) {
    window.requestIdleCallback(workloop, { timeout: 1000 });
  }
};
requestIdleCallback(workloop, { timeout: 1000 });

上面定義了一個任務隊列 taskQueue,並定義了 workloop 函數,其中採用 window.requestIdleCallback(workloop, { timeout: 1000}) 去執行 taskQueue 中的任務。每個任務中僅僅做了 console.log、sleep(3) 的工作,時間是非常短的(大約 3ms 多一點),瀏覽器計算此幀中還剩餘 15.5ms,足以一次執行完這三個任務,因此在此幀的空閒時間中,taskQueue 中定義的三個任務均執行完畢。打印結果如下: 

多幀執行

將 task1、task2、task3 中的睡眠時間提高至 10ms:

const sleep = (delay) => {
  const start = Date.now();
  while (Date.now() - start <= delay) {}
};
const taskQueue = [
  () => {
    console.log("task1 start");
    sleep(10);
    console.log("task1 end");
  },
  () => {
    console.log("task2 start");
    sleep(10);
    console.log("task2 end");
  },
  () => {
    console.log("task3 start");
    sleep(10);
    console.log("task3 end");
  },
];
const performUnitWork = () => {
  taskQueue.shift()();
};
const workloop = (deadline) => {
  console.log(`此幀的剩餘時間爲: ${deadline.timeRemaining()}`);
  while (
    (deadline.timeRemaining() > 0 || deadline.didTimeout) &&
    taskQueue.length > 0
  ) {
    performUnitWork();
  }
  if (taskQueue.length > 0) {
    window.requestIdleCallback(workloop, { timeout: 1000 });
  }
};
requestIdleCallback(workloop, { timeout: 1000 });

每個任務的時間被提高到 10ms 之後,在執行第 1 個任務時還能在第一幀剩餘的時間裏完成,在準備執行第 2 個任務時,雖然剩餘的時間(還剩 5ms 左右)不夠 10ms,但由於瀏覽器並不知道回調函數會執行多久,所以依然還是會在此幀內執行第 2 個任務(這也會導致下一幀的渲染延遲),到第 3 個任務時,當前幀肯定是已經沒有空餘時間了,那麼就再次調用 requestIdleCallback 申請下一個時間片。打印結果如下: 

可以明顯的看出任務 1、2 是在第一個幀內完成的,任務 3 在第二個。可能有人會好奇爲什麼第二幀的剩餘時間和第一幀差那麼多,這裏可以理解爲瀏覽渲染每幀的開始時間是不受渲染任務影響的,是固定不變 16ms 爲一週期(60hz 刷新頻率下),也就是說執行第 2 個任務超時的那幾毫秒不會推遲第二幀的開始時間,或者可以理解第 2 個任務(搶奪)了這第二幀一些時間,這裏畫了個圖,可以幫助大家更好的理解這個問題:

由此看來,應該避免在 requestIdleCallback 中執行過長時間的任務,否則可能會阻塞頁面渲染,以及頁面交互。當然也不建議在 requestIdleCallback 裏再操作 DOM,這樣會導致頁面再次重繪。DOM 操作建議在 rAF 中進行。同時,操作 DOM 所需要的耗時是不確定的,因爲會導致重新計算佈局和視圖的繪製,所以這類操作不具備可預測性。

Promise 也不建議在這裏面進行,因爲 Promise 的回調屬性 Event loop 中優先級較高的一種微任務,會在 requestIdleCallback 結束時立即執行,不管此時是否還有富餘的時間,這樣有很大可能會讓一幀超過 16 ms。

OK, requestIdleCallback 的基本信息也介紹完了,後面開始重點講講 react fiber 是如何搭配 requestIdleCallback 構建出 fiber tree 的。

四、React fiber 執行原理

Fiber Tree 的構建過程,實際上也是 diff 的過程,也就是 effect 的收集過程,此過程會找出所有節點的變更,如節點新增、刪除、屬性變更等,這些變更 react 統稱爲副作用(effect),隨着所有的節點(工作單元)在幀空閒時間逐個執行完畢,最後產出的結果是 effect list,從中可以知道哪些節點更新、哪些節點增加、哪些節點刪除了。

4.1 任務調度

React fiber 的構建的過程並不是一蹴而就的,它以每個 fiber 作爲一個工作單元,進行工作循環,工作循環中每次處理一個任務(工作單元),處理完畢有一次喘息的機會:

// Flush asynchronous work until there's a higher priority event
while (nextUnitOfWork !== null && !shouldYieldToRenderer()) {
  nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}

shouldYieldToRenderer 就是看時間用完了沒,沒用完的話繼續處理下一個任務,用完了就結束,把時間控制權還給主線程,等下一次 requestIdleCallback 回調再接着做。但如果當前渲染執行很長一段時間後還未結束,那麼就不再會喘息,而是一次性把剩餘工作全部做完。

if (!isYieldy) {
  // Flush work without yielding
  while (nextUnitOfWork !== null) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }
}

React Fiber 的工作調度與瀏覽器的核心交互流程如下: 

4.2 遍歷流程

Fiber Tree 構建的遍歷順序,它會以舊的 fiber tree 爲藍本,把每個 fiber 作爲一個工作單元,自頂向下逐節點構造 workInProgress tree(構建中的新 fiber tree)

具體過程如下:

  1. 從頂點開始遍歷

  2. 如果有子節點,先遍歷子節點;

  3. 如果沒有子節點,則看有沒有兄弟節點,有則遍歷兄弟節點,並把 effect 向上歸併

  4. 如果沒有兄弟節點,則看有沒有父兄弟節點,有則遍歷父兄弟節點

  5. 如果沒有都沒有了,那麼遍歷結束

其實就是一個深度優先的遍歷

可以先看看繼續看看任務調度中 performUnitOfWork 大致的實現:

function performUnitOfWork(fiber: Fiber, topWork: Fiber) {
  next = beginWork(current, workInProgress, nextRenderExpirationTime);
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    next = completeUnitOfWork(workInProgress);
  }
  return next
}

這一塊代碼可以看出下一個工作單元是的確定,是由當前工作單元 beginWork 之後得到的,這也合情合理,畢竟沒執行過當前工作單元,也就無法得知有無子節點生成。那我們繼續看看 beginWork 中是如何判斷下一個工作單元的。

switch (workInProgress.tag) {
  case HostComponent: {
    return updateHostComponent(current, workInProgress, renderExpirationTime);
  }  
    case ClassComponent: {
        return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderExpirationTime
    );
  }
  case FunctionComponent: {
    return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderExpirationTime
    );
  }
  // ...
}

可以看到 beginWork 里根據不同的 fiber 節點類型執行了不同的函數來獲得結果,似乎這一層也不能很清晰的看出來是怎麼確定出下一個工作單元的,由於這一塊實際邏輯比較複雜,就不再深入展示了,具體實驗之後會發現,遍歷邏輯大致如下:

// 如果存在子節點,那麼下一個待處理的就是子節點
if (fiber.child) {
  return fiber.child;
}
// 沒有子節點了,上溯查找兄弟節點
let temp = fiber;
while (temp) {
  completeWork(temp);
  // 到頂層節點了, 退出
  if (temp === topWork) {
    break;
  }
  // 找到,下一個要處理的就是兄弟節點
  if (temp.sibling) {
    return temp.sibling;
  }
  // 沒有, 繼續上溯
  temp = temp.return;
}

4.3 Reconciliation

瞭解了遍歷流程與任務調度方法之後,接下來就是就是我們熟知的 Reconcilation 階段了(爲了方便理解,這裏不區分 Diff 和 Reconcilation, 兩者是同一個東西)。思路和 Fiber 重構之前差別不大,只不過這裏不會再遞歸去比對、而且不會馬上提交變更

具體過程如下(以組件節點爲例):

  1. 如果當前節點不需要更新,直接把子節點 clone 過來,跳到 5;要更新的話打個 tag

  2. 更新當前節點狀態(props, state, context 等)

  3. 調用 shouldComponentUpdate(),false 的話,跳到 5

  4. 調用 render() 獲得新的子節點,併爲子節點創建 fiber(創建過程會盡量複用現有 fiber,子節點增刪也發生在這裏)

  5. 如果沒有產生 child fiber,該工作單元結束,把 effect list 歸併到 return,並把當前節點的 sibling 作爲下一個工作單元;否則把 child 作爲下一個工作單元

  6. 如果沒有剩餘可用時間了,等到下一次主線程空閒時纔開始下一個工作單元;否則,立即開始做

  7. 如果沒有下一個工作單元了(回到了 workInProgress tree 的根節點),第 1 階段結束,進入 pendingCommit 狀態

實際上是 1-6 的工作循環,7 是出口,工作循環每次只做一件事,做完看要不要喘口氣。工作循環結束時,workInProgress tree 的根節點身上的 effect list 就是收集到的所有 side effect(因爲每做完一個都向上歸併)

BeginWork

再回到 beginWork 具體看看它是如何對 Fiber 進行執行的(簡化版):

function beginWork(fiber: Fiber): Fiber | undefined{
  // 宿主節點diff
  case HostComponent: {
    return updateHostComponent(current, workInProgress, renderExpirationTime);
  }  
  // 類組件節點diff
    case ClassComponent: {
        return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderExpirationTime
    );
  }
  // 函數組件節點diff
  case FunctionComponent: {
    return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderExpirationTime
    );
  }
  // ... 其他類型節點,省略
}

宿主節點比對:

function diffHostComponent(fiber: Fiber) {
  // 新增節點
  if (fiber.stateNode == null) {
    fiber.stateNode = createHostComponent(fiber);
  } else {
    updateHostComponent(fiber);
  }
  const newChildren = fiber.pendingProps.children;
  // 比對子節點
  diffChildren(fiber, newChildren);
}

類組件節點比對也差不多:

function diffClassComponent(fiber: Fiber) {
  // 創建組件實例
  if (fiber.stateNode == null) {
    fiber.stateNode = createInstance(fiber);
  }
  if (fiber.hasMounted) {
    // 調用更新前生命週期鉤子
    applybeforeUpdateHooks(fiber);
  } else {
    // 調用掛載前生命週期鉤子
    applybeforeMountHooks(fiber);
  }
  // 渲染新節點
  const newChildren = fiber.stateNode.render();
  // 比對子節點
  diffChildren(fiber, newChildren);
  fiber.memoizedState = fiber.stateNode.state;
}

子節點比對:

function diffChildren(fiber: Fiber, newChildren: React.ReactNode) {
  let oldFiber = fiber.alternate ? fiber.alternate.child : null;
  // 全新節點,直接掛載
  if (oldFiber == null) {
    mountChildFibers(fiber, newChildren);
    return;
  }
  let index = 0;
  let newFiber = null;
  // 新子節點
  const elements = extraElements(newChildren);
  // 比對子元素
  while (index < elements.length || oldFiber != null) {
    const prevFiber = newFiber;
    const element = elements[index];
    const sameType = isSameType(element, oldFiber);
    if (sameType) {
      newFiber = cloneFiber(oldFiber, element);
      // 更新關係
      newFiber.alternate = oldFiber;
      // 打上Tag
      newFiber.effectTag = UPDATE;
      newFiber.return = fiber;
    }
    // 新節點
    if (element && !sameType) {
      newFiber = createFiber(element);
      newFiber.effectTag = PLACEMENT;
      newFiber.return = fiber;
    }
    // 刪除舊節點
    if (oldFiber && !sameType) {
      oldFiber.effectTag = DELETION;
      oldFiber.nextEffect = fiber.nextEffect;
      fiber.nextEffect = oldFiber;
    }
    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }
    if (index == 0) {
      fiber.child = newFiber;
    } else if (prevFiber && element) {
      prevFiber.sibling = newFiber;
    }
    index++;
  }
}

上面的代碼很粗糙地還原了 Reconciliation 的過程, 但是對於我們理解 React 的基本原理已經足夠了. 這裏引用一下 Youtube: Lin Clark presentation in ReactConf 2017 的 Slide,來還原 Reconciliation 的過程。

上圖是 Reconciliation 完成後的狀態,左邊是舊樹,右邊是 WIP 樹。對於需要變更的節點,都打上了'標籤'。在提交階段,React 就會將這些打上標籤的節點應用變更。

雙緩衝技術

雙緩衝技術(double buffering),以 current tree 爲主,workInProgress tree 爲輔。

雙緩衝具體指的是 workInProgress tree 構造完畢,得到的就是新的 fiber tree,然後喜新厭舊的把 current 指針指向 workInProgress tree,把舊的 fiber tree 放在一邊。

這樣做的好處:

每個 fiber 上都有個 alternate 屬性,也指向一個 fiber,創建 workInProgress 節點時優先取 alternate,沒有的話就創建一個:

let workInProgress = current.alternate;
if (workInProgress === null) {
  // We use a double buffering pooling technique because we know that we'll
  // only ever need at most two versions of a tree. We pool the "other" unused
  // node that we're free to reuse. This is lazily created to avoid allocating
  // extra objects for things that are never updated. It also allow us to
  // reclaim the extra memory if needed.
  // 沒有就創建一個
  workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode
    );
  //...這裏很有意思
  workInProgress.alternate = current;
  current.alternate = workInProgress;
} else {
  // We already have an alternate.
  // Reset the effect tag.
  workInProgress.effectTag = NoEffect;
  // The effect list is no longer valid.
  workInProgress.nextEffect = null;
  workInProgress.firstEffect = null;
  workInProgress.lastEffect = null;
}

如註釋指出的,fiber 與 workInProgress 互相持有引用,“喜新厭舊” 之後,舊 fiber 就作爲新 fiber 更新的預留空間,達到複用 fiber 實例的目的

副作用的收集和提交

接下來就是將所有打了 Effect 標記的節點串聯起來,這個可以在 completeWork 中做, 例如:

function completeWork(fiber) {
  const parent = fiber.return;
  // 到達頂端
  if (parent == null || fiber === topWork) {
    pendingCommit = fiber;
    return;
  }
  if (fiber.effectTag != null) {
    if (parent.nextEffect) {
      parent.nextEffect.nextEffect = fiber
    } else {
      parent.nextEffect = fiber
    }
  } else if (fiber.nextEffect) {
    parent.nextEffect = fiber.nextEffect
  }
}

將所有副作用提交了

function commitAllWork(fiber) {
  let next = fiber;
  while (next) {
    if (fiber.effectTag) {
      // 提交,偷一下懶,這裏就不展開了
      commitWork(fiber);
    }
    next = fiber.nextEffect;
  }
  // 清理現場
  pendingCommit = nextUnitOfWork = topWork = null;
}

總結來說,就是通過每個節點更新結束時向上歸併 effect list 來收集任務結果,reconciliation 結束後,根節點的 effect list 裏會記錄包括 DOM change 在內的所有 side effect,最後把所有副作用應用到真實 DOM 上。

4.4 如何中斷 / 斷點恢復

中斷:檢查當前正在處理的工作單元,保存當前成果(firstEffect, lastEffect),修改 tag 標記一下,迅速收尾並再開一個 requestIdleCallback,下次有機會再做

斷點恢復:下次再處理到該工作單元時,看 tag 是被打斷的任務,接着做未完成的部分或者重做

五、總結

其實稍一細想,從 Stack reconciler 到 Fiber reconciler,源碼層面就是幹了一件遞歸改循環的事情,把任務拆分成一個個細小的工作單元,隨着任務調度抽空執行,直到整顆樹構建完成。所以 react fiber 所謂的性能提升只是將 reconciliation 中 diff 過程的優先級往後調了,只在瀏覽器空閒時執行,從而提升了用戶交互、動畫渲染相關的優先級,使瀏覽器能夠更快反饋用戶操作,使頁面減少卡頓感,但並不會減少 react 渲染所需的工作量,因此想要真正提升應用性能還是得靠寫好代碼,減少不必要的渲染纔行呀。

尾聲

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/gz7_StDD151Qnt-ziSfmSw