性能優化(一):時間分片(Time Slicing):讓你的應用在高負載下“永不卡頓”的秘密

性能優化(一):時間分片(Time Slicing):讓你的應用在高負載下“永不卡頓”的秘密

引子:那張讓你瀏覽器崩潰的“無限列表”

想象一個場景:你需要渲染一個包含一萬個項目的列表。在我們的“看不見”的應用中,這可能是一個包含一萬個節點的Virtual DOM樹。

我們目前在第三章實現的render函數會怎么做?它會陷入一個巨大的、同步的遞歸調用中:

  1. createDom('ul')
  2. 循環一萬次 createDom('li')
  3. 在循環中,再調用 createDom('TEXT_ELEMENT')
  4. 將一萬個<li>節點逐一appendChild<ul>
  5. 最后,將這個巨大的<ul> appendChild到容器中。

這個過程可能需要幾百毫秒,甚至幾秒鐘。在這段時間里,你的JavaScript代碼將完全霸占瀏覽器的主線程

主線程是瀏覽器中一個極其繁忙的“單身漢”,它不僅要執行JavaScript,還要負責很多其他重要工作:

  • UI渲染:解析HTML/CSS,計算布局(重排),繪制像素(重繪)。
  • 用戶交互:響應用戶的點擊、滾動、輸入等事件。
  • 處理網絡請求響應定時器等等。

當你的JS代碼長時間霸占主線程時,這位“單身漢”就沒空去做其他任何事了。結果就是:

  • 頁面完全凍結,CSS動畫停止。
  • 用戶點擊按鈕、滾動頁面,毫無反應。
  • 瀏覽器甚至可能會彈出一個“頁面未響應”的警告框。

這就是主線程阻塞(Main Thread Blocking),它是導致Web應用性能差、體驗卡頓的罪魁禍首。

那么,問題來了:我們能否像一個體貼的同事一樣,不一次性把工作全做完,而是把一個大任務,分割成許多微小的小任務?每完成一小部分工作,我們就主動“讓出”主線程,給瀏覽器一個“喘息”的機會去處理UI渲染和用戶交互。當瀏覽器忙完了,再通知我們繼續處理下一個小任務。

這種“你好我好大家好”的協作式調度思想,就是時間分片(Time Slicing)。而這,也正是React Fiber架構能夠讓復雜應用保持流暢的“秘密武器”。


第一幕:瀏覽器的“空閑時間” - requestIdleCallback

要實現時間分片,我們需要一個機制來告訴我們:“嘿,瀏覽器現在有空,你可以來做點不那么緊急的工作了。”

幸運的是,瀏覽器提供了一個標準API來做這件事:requestIdleCallback

它的用法很簡單:

requestIdleCallback(myWorkFunction);

你傳遞給它一個回調函數(myWorkFunction),瀏覽器會在當前幀的渲染工作完成后,如果還有剩余時間,就去執行這個回調。

更強大的是,這個回調函數會接收一個deadline對象作為參數。

function myWorkFunction(deadline) {// 只要還有剩余時間,并且我還有工作要做...while (deadline.timeRemaining() > 0 && tasks.length > 0) {doNextTask();}// 如果時間用完了,但工作還沒做完,就再預約下一次空閑時間if (tasks.length > 0) {requestIdleCallback(myWorkFunction);}
}

deadline.timeRemaining()方法返回一個數字,表示當前幀還剩下多少毫秒的空閑時間。這讓我們能夠精確地控制每個工作片段的執行時長,確保不會超時而再次阻塞主線程。

如果瀏覽器不支持requestIdleCallback怎么辦?
我們可以用setTimeout(callback, 0)來進行優雅降級。setTimeout(..., 0)并不會真的立即執行,而是將回調函數放入宏任務隊列的末尾,相當于主動讓出一次主線程,等所有微任務和UI渲染結束后再執行。雖然它無法告訴我們“還剩多少時間”,但它依然實現了“讓出控制權”的核心目的。


第二幕:改造渲染引擎 - 從“遞歸”到“循環”

我們當前的render函數是遞歸的。遞歸調用一旦開始,就必須執行到棧空為止,無法中途暫停。要實現可中斷的渲染,我們必須將遞歸算法,改造成一個循環(while loop)算法

這正是React從舊版的Stack Reconciler到新版的Fiber Reconciler所做的最核心的改變。

我們將引入一個類似Fiber的數據結構。一個Fiber節點,不僅包含了VNode的信息,還通過parent, child, sibling指針,將整棵樹變成了一個可以迭代遍歷的鏈表

fiber.ts

// 文件: /src/v13/types/fiber.ts
import { VNode, Props } from '../v9/types/vdom'; // 假設之前的類型定義在v9export interface Fiber {// VNode的信息type: VNode['type'];props: Props;// 對應的真實DOM節點dom: Node | null;// Fiber間的關系指針parent?: Fiber;child?: Fiber;sibling?: Fiber;// 其他信息,比如用于Diff算法alternate?: Fiber; // 指向舊的Fiber節點effectTag?: 'UPDATE' | 'PLACEMENT' | 'DELETION'; // 標記這個節點需要做什么DOM操作
}

現在,我們的渲染過程將被拆分為兩個階段:

  1. 渲染階段(Render Phase): 這個階段是異步的、可中斷的。我們在這個階段構建Fiber樹,并找出所有需要進行的DOM更新(通過Diff算法)。這個過程不會有任何實際的DOM操作。
  2. 提交階段(Commit Phase): 這個階段是同步的、不可中斷的。一旦開始,它會一次性地將所有計算好的更新,應用到真實DOM上,確保UI的一致性。

實現一個“工作循環”調度器

我們將創建一個調度器,它維護一個任務隊列,并使用requestIdleCallback來驅動一個workLoop

scheduler.ts

// 文件: /src/v13/scheduler.ts
import { Fiber } from './types/fiber';
import { VNode } from '../v9/types/vdom';let nextUnitOfWork: Fiber | null = null; // 下一個要處理的工作單元
let workInProgressRoot: Fiber | null = null; // 當前正在構建的Fiber樹的根
let commitQueue: Fiber[] = []; // 需要提交的DOM操作隊列// 假的DOM操作和reconcile函數,僅為演示
function createDomForFiber(fiber: Fiber): Node {const dom = fiber.type === "TEXT_ELEMENT"? document.createTextNode(""): document.createElement(fiber.type as string);// ... apply propsreturn dom;
}
function reconcileChildren(wipFiber: Fiber, elements: VNode[]) {// 簡化邏輯:為每個child創建一個Fiberlet index = 0;let prevSibling: Fiber | null = null;while (index < elements.length) {const element = elements[index];const newFiber: Fiber = {type: element.type,props: element.props,dom: null,parent: wipFiber,effectTag: 'PLACEMENT',};if (index === 0) {wipFiber.child = newFiber;} else if (prevSibling) {prevSibling.sibling = newFiber;}prevSibling = newFiber;index++;}
}// 初始化渲染或更新
export function scheduleUpdate(rootFiber: Fiber) {workInProgressRoot = rootFiber;nextUnitOfWork = rootFiber;requestIdleCallback(workLoop);
}function workLoop(deadline: IdleDeadline) {let shouldYield = false;while (nextUnitOfWork && !shouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork);shouldYield = deadline.timeRemaining() < 1; // 留一點緩沖時間}// 如果工作全部完成,就進入提交階段if (!nextUnitOfWork && workInProgressRoot) {commitRoot();}// 如果時間用完了但工作還沒完,繼續預約if (nextUnitOfWork) {requestIdleCallback(workLoop);}
}function performUnitOfWork(fiber: Fiber): Fiber | null {// 1. "渲染"當前Fiber://    - 創建DOM節點(但先不掛載)//    - 根據children創建子Fiberif (!fiber.dom) {fiber.dom = createDomForFiber(fiber);}reconcileChildren(fiber, fiber.props.children || []);// 如果有effectTag,加入提交隊列if (fiber.effectTag) {commitQueue.push(fiber);}// 2. 返回下一個工作單元://    - 優先返回子節點if (fiber.child) {return fiber.child;}//    - 如果沒有子節點,返回兄弟節點let nextFiber = fiber;while (nextFiber) {if (nextFiber.sibling) {return nextFiber.sibling;}//    - 如果都沒有,返回"叔叔"節點(父節點的兄弟節點)nextFiber = nextFiber.parent!;}return null; // 全部完成
}function commitRoot() {// 這是一個同步過程commitQueue.forEach(fiber => {let parentFiber = fiber.parent;while (!parentFiber?.dom) {parentFiber = parentFiber?.parent;}const parentDom = parentFiber.dom;if (fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {parentDom.appendChild(fiber.dom);}// ... handle UPDATE and DELETION});// 清空commitQueue = [];workInProgressRoot = null;
}

整合到主流程

現在,我們有了一個全新的、異步的render函數。

main.ts

// 文件: /src/v13/main.ts
import { createElement } from '../v9/createElement';
import { VNode } from '../v9/types/vdom';
import { scheduleUpdate } from './scheduler';
import { Fiber } from './types/fiber';function render(element: VNode, container: HTMLElement) {const rootFiber: Fiber = {type: container.tagName.toLowerCase(),dom: container,props: {children: [element],},parent: undefined,child: undefined,sibling: undefined,alternate: undefined, // 初始渲染沒有舊Fiber};scheduleUpdate(rootFiber);
}// --- 演示 ---
const container = document.getElementById('root');// 創建一個非常大的VNode樹
const listItems = Array.from({ length: 10000 }, (_, i) => createElement('li', null, `Item ${i + 1}`)
);
const hugeList = createElement('ul', null, ...listItems);console.log("Starting asynchronous render...");
if (container) {render(hugeList, container);
}console.log("Render scheduled. Main thread is NOT blocked.");
console.log("You can click buttons or do other things now.");// 在瀏覽器的DevTools Performance面板,你會看到許多個小的"Task",
// 而不是一個長長的、紅色的"Long Task"。

這個新的渲染流程,與我們之前的同步遞歸模型,有著天壤之別:

  1. 可中斷workLoop在每次循環后都會檢查剩余時間。如果時間不足,它會保存當前進度(nextUnitOfWork),并讓出主線程。
  2. 可恢復:瀏覽器在下一次空閑時,會從上次中斷的地方(nextUnitOfWork)無縫地繼續執行。
  3. 優先級:雖然我們沒有實現,但這個架構允許我們為不同更新設置不同優先級(比如,用戶輸入的響應應該比數據拉取的渲染優先級更高)。React就是這么做的。

我們通過將遞歸轉為循環,并引入Fiber鏈表和requestIdleCallback調度器,成功地將一個可能耗時幾秒的宏任務,拆解成了幾百個耗時幾毫秒的微任務。在這幾毫秒的間隙中,瀏覽器可以自由地呼吸,響應用戶,從而創造出“永不卡頓”的流暢體驗。

結論:從“獨裁者”到“協作者”

時間分片的核心思想,是我們的JavaScript代碼從一個“獨裁的統治者”,轉變為一個“友好的協作者”。我們不再試圖一次性霸占主線程,直到所有工作完成,而是學會了觀察和等待,在瀏覽器不忙的時候,見縫插針地完成我們的工作。

這種轉變,是現代高性能前端框架的基石。它使得在處理復雜UI、大量數據、絢麗動畫時,依然能保證絲滑的用戶體驗成為可能。

核心要點:

  1. 長時間運行的JavaScript任務會阻塞主線程,導致頁面凍結、無法響應用戶交互。
  2. 時間分片通過將大任務分割成小塊,并在瀏覽器的空閑時間內執行,來解決主線程阻塞問題。
  3. requestIdleCallback是實現時間分片的原生API,它允許我們在瀏覽器空閑時執行低優先級任務。
  4. 為了實現可中斷和可恢復的渲染,必須將傳統的同步遞歸算法,重構為基于循環和鏈表(如Fiber)的異步迭代算法。
  5. 異步渲染流程被分為兩個階段:可中斷的渲染階段(Render Phase)和不可中斷的提交階段(Commit Phase),以保證UI更新的原子性和一致性。

我們已經掌握了如何讓應用在計算密集時保持流暢。但在實際應用中,性能的另一個殺手——內存——同樣不容小覷。在下一章 《性能優化(二):JS內存泄漏“探案”:從閉包到事件監聽的隱形殺手》 中,我們將化身“偵探”,學習如何使用Chrome DevTools等工具,去發現并修復那些隱藏在代碼中的、悄悄吞噬用戶內存的“內存泄漏”問題。敬請期待!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/91396.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/91396.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/91396.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

《C++》STL--list容器詳解

在 C 標準模板庫(STL)中&#xff0c;list 是一個非常重要的序列容器&#xff0c;它實現了雙向鏈表的數據結構。與 vector 和 deque 不同&#xff0c;list 提供了高效的插入和刪除操作&#xff0c;特別是在任意位置。本文將深入探討 list 容器的特性、使用方法以及常見操作。 文…

Day 28:類的定義和方法

DAY 28 類的定義和方法 知識點學習 1. 類的定義 在Python中&#xff0c;類是創建對象的模板。使用class關鍵字來定義一個類。類名通常采用首字母大寫的命名方式&#xff08;PascalCase&#xff09;。 # 最簡單的類定義 class MyClass:pass # 使用pass占位符類的定義就像是…

OSPF綜合實驗報告冊

一、實驗拓撲二、實驗要求1、R4為ISP&#xff0c;其上只配置IP地址&#xff1b;R4與其他所直連設備間均使用公有IP&#xff1b; 2、R3-R5、R6、R7為MGRE環境&#xff0c;R3為中心站點&#xff1b; 3、整個OSPF環境IP基于172.16.0.0/16劃分&#xff1b;除了R12有兩個環回&#x…

網絡層6——內部網關協議RIP、OSPF(重點)

目錄 一、基本概念 1、理想的路由算法應具備的特點 2、分層次的路由選擇協議 二、內部網關協議RIP 1、特點 2、路由交換信息 3、距離向量算法 4、壞消息傳送慢問題 5、RIP報文格式 三、內部網關協議OSPF 1、特點 2、其他特點 3、自治系統區域劃分 4、OSPF的5中分…

同品牌的系列廣告要如何保證宣傳的連貫性?

對于品牌的系列廣告而言&#xff0c;內容的連貫性十分重要。如果系列廣告之間缺乏內在聯系&#xff0c;不僅會削弱品牌形象的統一性&#xff0c;還可能導致用戶的認知混亂。保證宣傳內容的連貫性不是讓每則廣告完全相同&#xff0c;而是在變化中保持核心要素的一致性。我們該如…

深度學習:激活函數Activaton Function

一、為什么需要激活函數&#xff1f;神經網絡本質上是多個線性變換&#xff08;矩陣乘法&#xff09;疊加。如果沒有激活函數&#xff0c;即使疊加多層&#xff0c;整體仍等價于一個線性函數&#xff1a;這樣的網絡無法學習和擬合現實世界中復雜的非線性關系。激活函數的作用&a…

deepseek: 切分類和長函數到同名文件中

import re import sys import os import ast from tokenize import generate_tokens, COMMENT, STRING, NL, INDENT, DEDENT import iodef extract_entities(filename):"""提取類和函數到單獨文件"""with open(filename, r, encodingutf-8) as f…

新型融合肽遞送外泌體修飾可注射溫敏水凝膠用于骨再生

溫敏水凝膠因能模擬細胞外基質微環境&#xff0c;且具有原位注射性和形態適應性&#xff0c;在骨組織工程中應用廣泛。小腸黏膜下層&#xff08;SIS&#xff09;作為天然細胞外基質來源&#xff0c;富含 I 型和 III 型膠原蛋白及多種生物活性因子&#xff0c;其制備的水凝膠在組…

SPI接口的4種模式(根據時鐘極性和時鐘相位)

SPI&#xff08;Serial Peripheral Interface&#xff09; 接口根據時鐘極性&#xff08;CPOL&#xff09;和時鐘相位&#xff08;CPHA&#xff09;的不同組合&#xff0c;共有 4種工作模式。這些模式決定了數據采樣和傳輸的時序關系&#xff0c;是SPI通信中必須正確配置的關鍵…

Java:高頻面試知識分享2

HashSet 和 TreeSet 的區別&#xff1f;底層實現&#xff1a;HashSet 基于 HashMap 實現&#xff0c;使用哈希表存儲元素&#xff1b;TreeSet 基于 TreeMap&#xff0c;底層為紅黑樹。元素順序&#xff1a;HashSet 無序&#xff1b;TreeSet 會根據元素的自然順序或傳入的 Compa…

C語言習題講解-第九講- 常見錯誤分類等

C語言習題講解-第九講- 常見錯誤分類等1. C程序常見的錯誤分類不包含&#xff1a;&#xff08; &#xff09;2. 根據下面遞歸函數&#xff1a;調用函數 Fun(2) &#xff0c;返回值是多少&#xff08; &#xff09;3. 關于遞歸的描述錯誤的是&#xff1a;&#xff08; &#x…

A?算法(A-star algorithm)一種在路徑規劃和圖搜索中廣泛使用的啟發式搜索算法

A?A*A?算法&#xff08;A-star algorithm&#xff09;是一種在路徑規劃和圖搜索中廣泛使用的啟發式搜索算法&#xff0c;它結合了Dijkstra算法的廣度優先搜索思想和啟發式算法的效率優勢&#xff0c;能夠高效地找到從起點到終點的最短路徑。 1. 基本原理 A*算法的核心是通過估…

UniappDay06

1.填寫訂單-渲染基本信息 靜態結構&#xff08;分包&#xff09;封裝請求API import { http } from /utils/http import { OrderPreResult } from /types/orderexport const getmemberOrderPreAPI () > {return http<OrderPreResult>({method: GET,url: /member/orde…

論文略讀:GINGER: Grounded Information Nugget-Based Generation of Responses

SIGIR 2025用戶日益依賴對話助手&#xff08;如 ChatGPT&#xff09;來滿足多種信息需求&#xff0c;這些需求包括開放式問題、需要推理的間接回答&#xff0c;以及答案分布在多個段落中的復雜查詢RAG試圖通過在生成過程中引入檢索到的信息來解決這些問題但如何確保回應的透明性…

從內部保護你的網絡

想象一下&#xff0c;你是一家高端俱樂部的老板&#xff0c;商務貴賓們聚集在這里分享信息、放松身心。然后假設你雇傭了最頂尖的安保人員——“保鏢”——站在門口&#xff0c;確保你準確掌握所有進出的人員&#xff0c;并確保所有人的安全。不妨想象一下丹尼爾克雷格和杜安約…

Redis 中 ZipList 的級聯更新問題

ZipList 的結構ZipList 是 Redis 中用于實現 ZSet 的壓縮數據結構&#xff0c;其元素采用連續存儲方式&#xff0c;具有很高的內存緊湊性。ZipList 結構組成如下&#xff1a;zlbytes&#xff1a;4字節&#xff0c;記錄整個ziplist的字節數zltail&#xff1a;4字節&#xff0c;記…

【蒼穹外賣項目】Day05

&#x1f4d8;博客主頁&#xff1a;程序員葵安 &#x1faf6;感謝大家點贊&#x1f44d;&#x1f3fb;收藏?評論?&#x1f3fb; 一、Redis入門 Redis簡介 Redis是一個基于內存的 key-value 結構數據庫 基于內存存儲&#xff0c;讀寫性能高適合存儲熱點數據&#xff08;熱…

語音識別dolphin 學習筆記

目錄 Dolphin簡介 Dolphin 中共有 4 個模型&#xff0c;其中 2 個現在可用。 使用demo Dolphin簡介 Dolphin 是由 Dataocean AI 和清華大學合作開發的多語言、多任務語音識別模型。它支持東亞、南亞、東南亞和中東的 40 種東方語言&#xff0c;同時支持 22 種漢語方言。該模…

視頻生成中如何選擇GPU或NPU?

在視頻生成中選擇GPU還是NPU&#xff0c;核心是根據場景需求、技術約束和成本目標來匹配兩者的特性。以下是具體的決策框架和場景化建議&#xff1a; 核心決策依據&#xff1a;先明確你的“視頻生成需求” 選擇前需回答3個關鍵問題&#xff1a; 生成目標&#xff1a;視頻分辨率…

從豆瓣小組到深度洞察:一個基于Python的輿情分析爬蟲實踐

文章目錄 從豆瓣小組到深度洞察:一個基于Python的輿情分析爬蟲實踐 摘要 1. 背景 2. 需求分析 3. 技術選型與實現 3.1 總體架構 3.2 核心代碼解析 4. 難點分析與解決方案 5. 總結與展望 對爬蟲、逆向感興趣的同學可以查看文章,一對一小班教學:https://blog.csdn.net/weixin_…