性能优化(一):时间分片(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_…