news 2026/10/18 18:42:28

React性能优化实战复盘:从Profiler定位到渲染状态并发层优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React性能优化实战复盘:从Profiler定位到渲染状态并发层优化

前段时间团队内部做了一次React性能优化复盘,被同事问到一句:你具体做过哪些性能优化?我当时愣了一下——不是没做过,而是能拿上台面、能说明白因果的优化,远没有想象中多。想想也是,很多人聊React性能优化都能背出几个词:memo、useCallback、虚拟列表。但真到自己项目里,能把优化做对、做出可量化收益的,少之又少。

这篇文章我准备把自己在几个真实项目里动过手的性能优化完整复盘一遍。覆盖从定位瓶颈、组件渲染层优化、状态设计重构、路由与大数据量场景处理,到优化上线后的验证方式。既讲具体做法,也会解释每个优化背后的判断依据。老实说,React性能优化是个特别容易“努力了半天、效果为零”的领域,问题经常不在代码写法上,而在数据流设计、渲染边界、甚至后端返回结构上。希望这篇文章能让准备面试的人有真实案例可讲,也能让正在被性能问题折磨的人找到一条可复现的排查路径。

1. 接到性能需求后的第一步永远是量化与定位

先泼一盆冷水:如果你接到一个“页面很卡”的需求,上来就开始改代码、套memo、分组件,大概率白忙。我自己踩过这个坑。某个数据中台项目,表格页每次切换筛选条件都要卡两秒,当时第一反应是表格渲染太慢,打算优化渲染逻辑,结果看了半天代码发现根本不是那么回事。

1.1 用Profiler找到“真正花钱”的组件,而不是“看起来花哨”的组件

React DevTools的Profiler是我第一个会打开的工具。它记录每次commit的耗时,能直接看到组件树的渲染耗时分布,还能查看单个组件是因为props变化、state变化还是父组件渲染被连带触发。

具体操作上,我的习惯是三步走:

  • 在页面最卡的操作路径上打点,开启Profiler录制。注意要把设置里的“Record why each component rendered while profiling”打开,这一步能拿到每个组件重新渲染的原因。
  • 重复操作三到五次,取耗时分布稳定的结果,而不是只录一次。因为React的提前中断和并发渲染会让单次结果有波动。
  • 按渲染耗时排序,重点看排名前列的组件树,逐个展开看是哪个props引用不稳定导致子组件全部跟着重渲染。

那个表格页的定位结果特别典型:耗时第一的不是表格本身,而是表格外层一个包着全部筛选条件的筛选容器。每次拖动滑块改变筛选值,这个容器的state一变,整个商品管理模块的两百多个组件全部跟着重渲染。表格本身因为props比较稳定,反而不是瓶颈。这个案例告诉我一个铁律:性能优化和代码规模无关,和触发频率与影响范围有关。

1.2 用Performance面板确认“卡”的性质:每秒帧数、长任务与用户体感

DevTools的Profiler能定位组件问题,但有些卡顿的开销不在React内,而在浏览器主线程的其他环节。遇到交互卡顿,我还会开浏览器Performance面板,录制操作过程,重点看几个指标:

  • 长任务(Long Task):如果主线程有连续超过50ms的任务,那用户一定会感觉到卡顿。
  • 每秒帧数(FPS):录一段滚动或动画操作,看帧率有没有明显掉到30fps以下。
  • 布局与绘制事件:如果长任务里大量是Recalculate Style和Layout,那问题可能不在React,而在CSS选择器、强制同步布局、图片解码上。

有一次遇到一个活动页,滚动时掉帧严重。Profiler显示组件渲染只有十几毫秒,完全健康。结果Performance面板里发现大量时间耗在img元素的图片解码上,加上页面用了多个全屏背景图,解码时机全挤在滚动一开始。最后把图片改成按需加载加缩略占位,滚动立刻顺滑了。

另外,对于线上真实用户,我建议埋点收集Web Vitals指标,特别是LCP和INP。INP(Interaction to Next Paint)就是用来衡量交互响应速度的,能在小流量实验里量化优化前后差异,比本地录制更有说服力。我自己的项目会在PerformanceObserver里单独采集长任务和INP上报,方便发布后对照效果。

2. 组件渲染层的优化:memo、缓存与重新渲染的边界感

定位完瓶颈,最常见的优化入口就是减少无意义的组件重渲染。但这一块的技巧含量比很多人以为的高很多。React.memo、useMemo、useCallback几乎成了性能优化的代名词,可用错的概率极高。

2.1 React.memo的作用边界:只有“父组件重渲染但props不变”时才真正有效

React.memo的基本作用就是让函数组件在props浅比较相等的情况下跳过重渲染。但有两个前提必须同时满足:第一,父组件确实在重渲染;第二,传入该子组件的props引用必须保持不变。

实际项目里最常见的翻车场景是:给子组件加了memo,但父组件里向它传了一个内联箭头函数。比如:

// 这样写,memo等于白给 <ExpensiveTable data={tableData} onRowClick={(id) => handleRowClick(id)} />

父组件每次渲染都会创建新的函数引用,memo的浅比较直接判定失败。正确写法是用useCallback包住函数,保证引用稳定。同理,如果传递的是对象或数组,也需要用useMemo或把数据拆分后只传需要的字段。

我自己的判断标准是:组件是否真的因为重渲染付出过高代价。如果组件本身渲染成本很低,强行加memo反而让React需要做一次额外的浅比较,这也是一次函数调用和比较,在超大组件树下是纯负收益。所以我在项目里往往只在两种情况下加memo:一种是大列表里的行组件,另一种是渲染成本高但props相对稳定的图表、富文本组件。

2.2 useCallback与useMemo的正确使用姿势:它们是“引用稳定工具”,不是“计算加速器”

很多人以为useMemo是用来“缓存计算结果、避免重复计算”的,这点没错,但它更常用于缓存引用类型,让子组件的memo比较得以通过。我给自己定的规则是这样的:

  • useCallback:用于保持函数引用稳定,尤其是它被传给被memo包裹的子组件、或者被放进effect依赖数组时。
  • useMemo:两种用途都常见,但我更看重的是它稳定引用类型的能力。如果仅是为了避免一次纯计算,先看看这个计算到底多重——简单的加减乘除和数组过滤,重算一次的开销远小于useMemo自身的依赖比较开销。

举个例子,某个项目里有个筛选面板,下拉选项是根据全量配置数据过滤出来的:

const filteredOptions = useMemo( () => allOptions.filter( (opt) => opt.category === currentCategory ), [allOptions, currentCategory] );

这里用useMemo真正的收益不是少了一次filter,而是filteredOptions拿到的数组引用在依赖不变时保持一致,避免下游列表组件和多个memo子组件全体重渲染。如果只是单纯传数组给一个不关心引用的组件,完全没必要用。

另外要提醒一个abuse场景:我在某公司项目里看到过一个开发把所有函数全部用useCallback包裹,理由是“保险”。结果因为useCallback的依赖数组写错,函数拿到的永远是旧闭包,排查了整整两个下午。useCallback和useMemo都是有依赖数组的,依赖声明不准确时会引入比性能更严重的问题——逻辑bug。

2.3 key的正确使用与列表重渲染的隐性成本

React文档从入门就在讲key要在列表里加,但实际项目里把index当key用的依然很多。index当key的代价是:列表项的顺序一旦变化(排序、中间插入),React会认为每个组件只是props变化了,从而复用原有组件实例,导致本不该被重建的DOM状态被错误复用,而且渲染范围会波及整个列表。

比如一个表格,操作列有展开状态、有临时输入内容,你用index当key,每次排序后表格内容错乱。这在业务上直接是bug,在性能上会带来整个列表项的组件复用和更新。

我自己在长列表渲染时的做法是:

  • 优先用业务数据里稳定的id作为key。
  • 如果数据确实没有唯一标识,组合几个字段生成稳定字符串再作为key。
  • 如果列表项的渲染成本很高且项之间完全独立,可以考虑给组件套一层React.memo,并用key把项分离成语义独立的单元。
2.4 自定义组件拆分:降低重渲染面积不等于砍组件数量

不少人在优化时的直觉是把大组件拆成很多小组件,觉得这样渲染会更轻。这个想法只对了一半。拆分的真正价值在于:让不同关注点的数据变化只影响自己所属的那部分组件。如果拆完以后,所有子组件的state和props都还挂在同一个父级上,那拆分毫无意义。

我自己见过一个经典反面案例:一个表单页面被拆成了十几个组件文件,但所有输入值都集中在一个顶层form data对象里,任何一步操作都会让整个表单全量重渲染。从文件结构看组件拆得很细致,但从渲染边界看,它就是一个无比巨大的组件。

正确做法是把数据粒度细拆,让“存数据的组件”尽可能靠近“消费数据的组件”。比如把每个输入项拆成独立受控组件,各自管理自己的值,只在失焦或提交时才把值合并到外层。这样任何一次输入动作影响到的重渲染范围就被锁死在一个小输入框内部了。

3. 状态与数据流设计:多数“卡到不能动”的根源比想象中更靠前

说个真实体验:大部分React性能问题,根子都出在状态设计上,而不是渲染写法上。页面“卡”,往往是因为一次很小的状态变化,通过层层传递,引发巨大的渲染风暴。

3.1 状态放在哪里,决定了重渲染的半径

React数据流是单向的,一个组件的state变化,会触发从它开始往下的所有子树重新渲染。所以一条核心原则是:状态要放在“恰好能覆盖消费它的最小组件层级上”。

遇到过的一个典型案例:某个后台管理系统,全局有一个当前用户信息对象,包含头像、组织、权限等一系列字段。有人图方便,把这个对象放进了全局状态管理库里,整个系统所有页面组件都订阅它。结果只要用户权限信息有一次刷新,顶栏、侧边栏、甚至整棵业务树全部重渲染。更吓人的是,很多组件根本不用这个user对象,只是因为在Provider嵌套下被顺带渲染了。

这个案例的修复方式并不高端:把用户信息拆分,基础资料在顶栏组件内自行请求,权限数据单独下沉到路由守卫层,只在需要权限判断的地方消费。渲染半径被砍掉一大圈,系统整体交互明显变轻。

3.2 Context滥用:消费者全体重渲染的陷阱

使用Context是React官方推荐的数据共享方式,但它有一个性能特性经常被忽略:当Context的value变化时,所有消费者组件都会强制重渲染,而且这个重渲染没有办法用React.memo拦截。

所以我自己的项目里用到Context时有三条约束:

  • value必须是稳定的,不会在每次父组件渲染时都生成新对象。这个用useMemo包一下就行,但依赖项千万别漏。
  • 消费面要尽可能窄。不要搞一个全局大而全的Context,往里面塞项目配置、语言包、用户信息、路由状态。最好按业务域拆分成多个独立Context。
  • 高频变化的状态尽量不要用Context。比如鼠标位置、输入实时值、滚动进度,这些会有极高频率的状态更新,塞进Context会让所有消费者一起遭殃。

某项目里有个音频播放器,播放进度放在一个全局Context里,结果每个动画组件、每个列表组件都跟着音频播放进度疯狂重渲染。后来我把进度状态提升到播放器组件内部,只在播放状态改变时向全局同步信息,整个页面的CPU占用率立刻降了一个档次。

3.3 useReducer不是用来“高大上”的,它是可控性的帮手

我在项目里倾向用一个简单的判断标准来决定用useState还是useReducer:如果状态更新逻辑只涉及单个值的原地修改,用useState足够;如果状态更新牵扯多个字段、有相互依赖的复合变化、或者需要撤销回退,用useReducer。

换到性能层面,useReducer本身不会带来渲染优化,但它能约束状态更新的集中性和有序性。在实际项目中,把零散的setState调用收拢到reducer里,能更容易发现“某次点击会连续触发十几次状态更新”这类问题。React 18之后,这类连续更新可以通过自动批处理合并,但业务上若在定时器或异步回调里散落setState,仍然会带来多次独立渲染。

3.4 数据不可变性与浅比较的联动

memo、useMemo、React.memo的浅比较都依赖一个前提:数据更新必须产生新的引用,而无关数据不能跟着变。这其实就是不可变数据带来的优势。

实际开发里我用Immer的简化写法来管理比较复杂嵌套的state更新。比如一个图表配置对象,里面有一层数据、二层样式、三层交互选项。如果用展开运算符一层层拷贝,很容易出现“改了第二层字段,第三层引用也变了”的现象,导致memo比较失效。用Immer之后,结构共享机制会保证只有修改过的节点产生新引用,其余节点沿用旧引用,这就能让React的浅比较精确命中“我只改了第二层”这件事。

4. 用户体感最重的三项:路由加载、图片与大数据量列表

组件渲染层的优化解决的是“交互更新时的卡顿”,但用户体感里的“首屏慢”“滚动卡”,很大程度归因于另外三项:路由按需加载、图片解码与加载策略、超大数据量的列表和表格。

4.1 路由级代码分割:把首屏资源体积降下来的第一招

React项目打包后如果只有一个巨大的bundle.js,用户就得等整包下载完才能看到页面。解决思路是路由级代码分割,用React.lazy和Suspense把每个路由页面打包成独立chunk,访问哪个路由就下载哪个页面。

代码实现非常简单:

const Dashboard = React.lazy(() => import('./pages/Dashboard')); const Settings = React.lazy(() => import('./pages/Settings'));

但里面有几个容易踩的坑:

  • 懒加载组件必须有Suspense包裹,并且要设置fallback。但fallback不要做成满屏转圈,最好做成与页面骨架相近的占位,避免路由切换时整页白屏闪一下。
  • 懒加载不要切得太碎。把每个组件文件都单独import是错误示范,这会产生大量小请求,HTTP开销反而更大。正确的粒度是路由级别,或者是一个完整业务域级别的模块。
  • 预加载策略。用户鼠标hover到某个导航项时,可以用动态import预加载目标路由chunk。这种“预测式预加载”能把跳转时的等待时间降到几乎为零。

我自己在做过的一个H5活动页里用了这种策略,首屏JS体积从600多KB降到180KB左右,首屏加载时间在低端安卓机上直接快了一倍还多。

4.2 图片处理:体积、解码时机与占位缺一不可

很多团队对图片的优化手段停留在“压缩一下”这个阶段。但实际上图片性能有四件套:尺寸、格式、加载时机、占位表现。

  • 尺寸:接口返回的图片一定要按展示尺寸裁剪,不要拿原图硬缩。一张1920宽的大图在列表缩略图里展示,消耗的内存和解码时间是肉眼可见的。
  • 格式:WebP或AVIF在同等画质下体积只有JPEG的一半甚至更小。兼容性方面做好picture标签多源回退即可。
  • 加载时机:使用loading="lazy"让视口外图片延迟加载,同时配合fetchpriority="high/low"控制关键图片的优先级。
  • 占位:用CSS aspect-ratio提前占好宽高,避免图片加载后页面布局跳动。布局跳动会造成额外的Layout开销,也会让用户感觉页面在抖动。

有一次优化一个电商店铺页,图片列表较长,直接全部懒加载并统一加上宽高占位,截图对比时LCP从4.2s降到了2.1s。这个优化没有碰任何React代码,纯粹是对图片策略的调整。

4.3 大数据量列表:从虚拟滚动到增量渲染

当列表项达到几百上千条时,一次性渲染全部DOM必然会卡。虚拟滚动是标准解,它只渲染视口内可见的列表项,滚动过程中通过计算偏移量来动态替换渲染内容。

我在项目里常用react-window,比较轻量。但真正想提醒的是虚拟滚动在业务里的边界问题:

  • 不定高列表项会让virtualization难度大增,需要动态测量行高,复杂度会显著上升。
  • 虚拟滚动会移除视口外的DOM,这对依赖DOM数量做某些统计、或者需要对列表进行全量搜索的场景有影响。需要额外处理。
  • 虚拟滚动组件内部处理了滚动行为后,滚动条样式和原生表现会有差异,设计走查时容易冒泡。

如果说虚拟滚动是“渲染面积缩小”,那增量渲染就是“渲染任务分批”。比如一个弹窗里要展示数百条日志,直接一次setState传几百条数据可能导致一次commit时间超过100ms。改成每帧渲染50条,连续几帧渲染完,用户感知到的就是“快速刷出来”,而不是“卡住一下再弹出来”。React 18提供了useDeferredValue和startTransition,刚好可以做这件事。

5. 并发特性在真实优化里的用法:startTransition与useDeferredValue

React 18引入并发特性后,性能优化多了一个维度:把紧急更新和过渡更新区分开。这是很多老项目没用上的潜力点。

5.1 搜索框卡顿的经典解法:用startTransition压低过滤任务的优先级

某个项目里有个搜索框,输入字符后要实时过滤一个上千条的数据列表。每敲一个字母,列表全量更新一次,键盘输入有明显延迟。这个问题的本质是:输入事件本身是紧急更新,但过滤列表其实是可以稍微延后的非紧急任务。两者挤在一次渲染周期里,紧急的被拖累了。

修复方式是用startTransition把过滤任务标记为过渡更新:

const [searchValue, setSearchValue] = useState(''); const [filteredList, setFilteredList] = useState(allData); function handleInputChange(e) { const current = e.target.value; setSearchValue(current); // 紧急更新,让输入框先响应 startTransition(() => { setFilteredList(filterAllData(current)); // 过渡更新,列表可以稍慢 }); }

这样用户输入字符时,输入框能立刻响应,列表过滤会在浏览器有空闲时再执行。实际体验上,字符不会再丢,列表偶发“慢半拍”反而更符合用户的注意节奏。

5.2 useDeferredValue与useMemo的配合:把“延迟值”用到极致

useDeferredValue的用途和startTransition类似,适合在不能直接控制更新来源时使用。它的意思是:获取一个延迟变化的版本,这个版本优先让路给紧急更新。比如列表组件的props直接来自父组件,不方便包startTransition时,可以这样处理:

const deferredSearchValue = useDeferredValue(searchValue); const displayList = useMemo( () => filterData(allData, deferredSearchValue), [allData, deferredSearchValue] );

这里要注意的一点是:useDeferredValue会让列表永远比输入慢一拍,如果用displayList同时去驱动受控组件展示,会出现“显示值和输入值不一致”的问题。所以我在项目里只会用它驱动非关键的展示数据,用于真正决定界面显示的受控值该用原值。

5.3 并发特性使用的一个反例:强行标记导致视觉闪烁

用startTransition前一定要想清楚业务接受度。有些更新虽然计算量大,但用户期待它“立刻发生”,比如点击Tab切换页面标题。如果把这个也标记成过渡更新,浏览器可能先渲染旧UI一段时间,用户会明显感觉到“点了没反应”,比卡顿更让人抓狂。实践中我的原则是:破坏性大的内容切换保持紧急更新,只有计算结果量级大、非首屏必须、失败后可重新触发的更新才适合丢进过渡队列。

6. 上线前的验证方法:别把性能优化做成自我感动

优化做完以后,最重要的一件事是验证。我见过太多团队“优化”完以后没有数据支撑,上没上线、效果如何全凭感觉。

6.1 本地对比基线:用同一台机器、同一条操作路径做前后对比

做性能对比时,环境一致性是第一位的。不同机器、不同浏览器版本、不同网络环境都会带来巨大误差。我的操作流程是固定一台开发机,无痕模式打开页面,禁用缓存,分别录优化前和优化后的Performance和Profiler结果。

以之前的表格页优化为例,优化前的数据是这样的:拖动筛选滑块,一次setState触发组件树重渲染总共263个组件,commit耗时约165ms,长任务明显,帧率掉到20fps。优化后将筛选值下沉到筛选容器内部、列表行组件加memo、函数引用用useCallback稳定,同样的操作只触发了与筛选直接相关的47个组件更新,commit耗时降到约39ms,帧率基本稳定在60fps。这份对比数据就是明确的优化证据。

6.2 线上指标:用INP、LCP和长任务占比衡量真实收益

本地的数据只能说明本地场景,线上真实网络和用户设备千差万别。我习惯让前端监控平台上预先接好Web Vitals上报,然后在小流量发布后对比优化前后的P75、P90数据。重点看三个:

  • LCP(Largest Contentful Paint):首屏主体内容的加载速度。
  • INP(Interaction to Next Paint):交互响应速度,替代了原先的FID指标。
  • Long Tasks数量:主线程被阻塞的次数和总时长。这个指标对“页面到底顺不顺滑”描述最直白。

上线一周后拉数据,我发现小流量分组的INP中位数从420ms降到了约200ms,长任务出现次数也下降了一半以上。这是一组能让产品、测试都能看懂的数字,比单纯说“我加了memo代码变优雅了”有说服力得多。

6.3 回归测试:性能优化不能引入可访问性和交互bug

优化代码上线前,还有一类测试不能省:功能回归。尤其是memo、useCallback这类优化,会改动组件的渲染时机和依赖关系,容易引发两个问题:

  • 组件不再更新:某个字段被加入useCallback依赖数组时漏掉,函数闭包捕获了旧值,导致列表数据不回显。
  • 交互状态错乱:长列表虚拟滚动后,组合框和复制粘贴行为异常,因为原来依赖DOM节点的全局状态在虚拟滚动下不存在了。

所以我在优化项目里会特地准备一个回归清单:列表筛选、详情回显、组件内输入值保持、浏览器后退行为。跑完回归再上线,不然出了线上事故,优化效果再好也是白搭。

优化做到这里,基本上把从定位、渲染层、状态层、加载层到验证的流程走完了一遍。如果把这段经历浓缩成一句话,那就是:React性能优化的第一课不是写代码,而是学会用数据定位谁在拖慢页面。工具会用以后,React.memo、useCallback、useDeferredValue这些手段才谈得上对症下药。

总有人问我,到底什么样的项目需要做性能优化?我的回答是,业务体感有卡顿就做,同时把优化动作建立在量化验证闭环里。如果做完能拿出LCP下降、INP下降、长任务清零的数据,而不是只说“感觉快了”,那这次优化就是实实在在的产出。希望这篇复盘对你有帮助,至少在你下次被问到“你在React里具体做过哪些性能优化”时,脑子里能浮现出几条完整的故事线和数据。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 18:41:58

Git核心原理与实战:从快照、分支到协作工作流

1. 从“代码存档”到“时光机”&#xff1a;Git到底解决了什么麻烦很多人第一次接触Git&#xff0c;是被“版本控制”这四个字吓退的。听起来像是某种只有大型团队才用得上的重型武器&#xff0c;跟个人开发者、写文档的、做设计的没什么关系。但实际情况恰恰相反——Git最初被…

作者头像 李华
网站建设 2026/10/18 18:41:31

Qt 5.9核心机制源码解析:信号槽、事件循环与QPainter深度实战

简介&#xff1a;本资源是面向C初学者与Qt入门开发者的实践型代码包&#xff0c;聚焦Qt 5.9核心机制的直观呈现&#xff0c;涵盖信号与槽、QWidget布局、自定义类封装及UI与逻辑分离等关键知识点。压缩包共8个文件&#xff0c;含3个cpp实现文件&#xff08;如main.cpp、widget.…

作者头像 李华
网站建设 2026/10/18 18:40:43

智慧农业集成管理系统实践:设备接入、阈值告警与可视化大屏

简介&#xff1a;这是一套面向农业信息化开发者及物联网应用方向的智慧农业集成管理系统源码&#xff0c;涵盖物联网监测、大数据分析、设备控制、地块管理、农事计划等典型业务。项目基于Java Web技术栈&#xff0c;采用分层开发模式&#xff0c;实体、接口、控制层与前端页面…

作者头像 李华
网站建设 2026/10/18 18:35:34

Fluent旋转机械流场分析与换热器仿真实战经验全解析

在接到“旋转机械流场分析换热器仿真”这类任务时&#xff0c;多数人第一反应是打开Fluent导入几何然后点计算。但实际做过几个完整项目后会发现&#xff0c;真正决定仿真价值的往往不是软件操作本身&#xff0c;而是对物理问题的拆解深度、网格策略的取舍、模型选择的依据&…

作者头像 李华
网站建设 2026/10/18 18:34:15

基于Spring Boot与WebSocket的Web远程控制系统毕业设计源码解析与实战

简介&#xff1a;本资源为基于Web远程控制系统的毕业设计完整资料&#xff0c;面向计算机相关专业学生及需要完成课程设计或毕业设计的学习者&#xff0c;帮助解决远程设备监控与控制类课题从选题到实现的全流程需求。压缩包内共1个docx文件&#xff0c;约1.08MB&#xff0c;内…

作者头像 李华
网站建设 2026/10/18 18:33:25

用Socket模拟滑动窗口协议:避开内核陷阱,掌握TCP核心机制

简介&#xff1a;面向计算机网络课程学习者与相关开发者的编程模拟资源&#xff0c;演示如何用socket建立TCP连接&#xff0c;在应用层还原数据链路层的1bit滑动窗口协议&#xff0c;适合课程实验与自学。资源共2个文件&#xff0c;均为C语言源文件&#xff0c;压缩包大小仅4KB…

作者头像 李华