☰
前端性能优化实战:定位瓶颈、消除卡顿的完整JavaScript指南
2026/10/8 3:15:48 网站建设 项目流程

1. 优化之前先止血:别再凭感觉改代码了

我接手过不少所谓"跑不动"的前端项目,打开控制台一看,FPS掉到个位数,滚动和纸糊的一样。多数人第一反应是"这个框架太重了""这个插件该换了",但真把框架拆了重写之后,问题大概率还在。为什么?因为绝大多数性能问题不在框架本身,而在我们写进去的每一行业务代码里。JavaScript性能优化这门功夫,练的不是某个大招,而是对运行时开销的敏感度。

这篇文章打算聊的,就是我在真实项目中用过的排查链路和优化手段,覆盖从代码写法、数据判断、DOM操作、Canvas渲染,到移动端适配、运行时报错处理这些高频场景。不管你是被线上性能事故逼到墙角的新手,还是正在给复杂中后台项目做体检的资深工程师,里面涉及的思路和代码都能直接拿走用。

先说一个反直觉的结论:很多时候,去掉一个"看着很优雅"的写法,比引入一整套优化方案更管用。JavaScript引擎(V8、JSC、SpiderMonkey)确实一年比一年快,但它不是魔法师——它擅长的是优化稳定的、可预测的代码,最怕的是你写出的代码形态反复变化,让它没法内联、没法隐藏类、没法做内联缓存。后面我会逐个场景拆开讲,到底什么样的代码形态是引擎喜欢的,什么样的写法是给引擎挖坑。

2. 性能清单:先量化,再动手

任何优化工作,没有量化之前都是玄学。我见过最典型的场景是:同事说"页面有点卡",然后直接开始用setTimeout到处乱包。这种操作除了让代码变难看,对性能没有任何帮助。正确做法是,先花几分钟把问题量化出来,确认瓶颈到底在哪儿。

2.1 用Performance面板建立基线

Chrome DevTools的Performance面板(现在叫Performance insights)是第一步。录制一段典型操作(比如滚动列表、打开弹窗、切换Tab),重点看三个指标:

  • FPS:页面每秒渲染帧数,稳定在50-60才算健康,低于30基本可以判定有持续卡顿。
  • Scripting时间:JavaScript执行占据主线程的时间。如果这里出现大段黄色块,说明计算逻辑有问题。
  • Rendering / Painting时间:紫色和绿色块对应的布局与绘制开销,过长说明DOM结构或样式触发了大量重排重绘。

我通常还会打开Memory面板采样两三次堆快照,对比内存使用量是否持续增长。如果是,基本可以断定写入了"内存只增不减"的逻辑——这在单页应用里很常见,比如全局事件监听器没解绑、setInterval没清掉、DOM引用被闭包长期持有。

采集完基线数据,再做优化,改完再测一次,用数据说话。没有这一步,后面所有优化都是无根之萍。

2.2 Performance API:把关键指标埋进线上

DevTools只能覆盖本地和测试环境,线上的真实性能数据,要靠Performance API来拿。我最常用的是performance.mark()配合PerformanceObserver:

// 标记一个异步任务开始 performance.mark('task-start'); fetch('/api/list') .then(res => res.json()) .then(data => { performance.mark('task-end'); performance.measure('list-fetch', 'task-start', 'task-end'); // 通过 PerformanceObserver 上报 });

这个方法特别适合定位"接口都返回了,页面怎么还在转圈"这类问题。接口耗时来自网络面板,但从接口返回到DOM渲染完成之间的这段主线程时间,只有靠measure才能量出来。我曾在一个项目里用这招定位到一个连续解析大JSON导致主线程阻塞3.2秒的问题——不看这段埋点数据,我可能一直在排查接口那边。

2.3 长任务监听:用户感受到的卡顿都藏在这里

PerformanceObserver还可以监听longtask事件,这是更精准的卡顿捕获方式:

const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { // entry.duration 超过 50ms 的任务都会列出来 console.log('长任务耗时:', entry.duration, entry.startTime); } }); observer.observe({ entryTypes: ['longtask'] });

"长任务"是浏览器定义的一个临界值:主线程上任何超过50毫秒的任务,都会被标记为长任务。为什么是50ms?因为浏览器要在100ms内响应用户输入才能让交互显得流畅,一个超过50ms的任务就已经占掉了半条命。把长任务一个个揪出来,再针对性地拆分、异步化,这对用户感知的提升是最直接的。

3. 代码写法里的隐形开销:判断、循环、作用域

这一节是重头戏。很多人优化性能喜欢先盯着打包配置和网络请求,但真正影响交互流畅度的,往往是代码层面的写法细节。热搜词里的"javascript判断数据类型""javascript函数"之所以高频出现,就是因为这些最基础的写法恰恰藏了最多的性能坑。

3.1 判断数据类型的开销差异

判断数据类型在业务代码里太常见了,常见到没人会去想它有没有性能问题。但实际测下来,几种方式的差异非常可观。

  • typeof:最快,但只能返回"undefined" "boolean" "number" "bigint" "string" "symbol" "function" "object",对null和数组没有区分度。
  • instanceof:需要沿着原型链向上查找,代价比typeof高一个数量级,而且跨iframe场景会误判。
  • Object.prototype.toString.call():最精确,但要创建临时字符串,性能是最差的一档。
  • Array.isArray():这是V8引擎专门优化的原生方法,判断数组的性价比非常高。

我实测过一个场景:一个列表页有5000条数据,每条渲染前要判断某个字段是字符串还是数组。用Array.isArray()比用Object.prototype.toString.call()整体快了近40毫秒。单次看着不起眼,但放在高频循环里就是质的差别。

所以在选择判断方式时,我的建议是:

判断目标推荐方式为什么
基础类型typeof零开销,语义直接
数组Array.isArray()引擎原生优化,准确
nullvalue === null显式判断
未知复杂类型Object.prototype.toString.call()兜底方案,仅在数据来源不可控时使用

另一个常见坑是连续三元判断和switch的性能差异。现代引擎对switch做了密集优化,当判断分支很多(比如8个以上)且命中分布不均时,switch通常会优于一串三元表达式。原因是switch可以被转换为跳转表,而三元运算符必须按顺序逐个比较。

3.2 循环里的"不可优化"陷阱

你在循环里做过的每一件小事,都会被放大数千倍。常见的有三类:

第一,在循环体内反复读取需要计算的属性。

// 不推荐:每次循环都调用一次 getLength() for (let i = 0; i < getLength(); i++) { // 业务逻辑 } // 推荐:长度只计算一次 const len = getLength(); for (let i = 0; i < len; i++) { // 业务逻辑 }

第二,用for...in遍历大数组。for...in不只遍历索引,还会把原型链上所有可枚举属性都过一遍。处理数组别用for...in,就用索引循环或者for...of。

第三,在循环里做字符串拼接。历史经验是"字符串拼接要用数组join",但现代引擎对+=做了大量优化,实测下来短字符串拼接两者差距不大。真正的杀手是在循环里一层套一层地创建新字符串,比如在循环中调用toUpperCase()、trim()这类产生新字符串的方法——它们每个都会分配内存。能提出循环外做的字符串处理,就一定要提出来。

3.3 函数、闭包和隐藏类:引擎如何理解你的代码

V8这类现代JavaScript引擎在编译时会做内联缓存(Inline Caches,简称IC)和隐藏类(Hidden Classes)优化。听起来很高级,但它对代码写法有很实际的要求:对象的形状要稳定。

什么意思?如果你反复给一个对象增删属性,或者对象的属性初始化顺序不固定,引擎只能不断创建新的隐藏类,导致每次属性访问都退化成字典查找,而不是偏移量读取。

// 不推荐:不同路径创建的对象属性顺序不一致 function createUser(id, name) { const user = { id: id }; if (name) { user.name = name; // 这个属性是运行时才加的 } return user; } // 推荐:属性一次性定义完整,顺序固定 function createUser(id, name) { const user = { id: id, name: name || '' }; return user; }

这条规则对需要构造大量对象的场景(比如渲染长列表时生成配置对象)影响尤为明显。同样逻辑下,"一次性初始化完整属性"的版本比"动态增加属性"的版本在长列表场景下性能平均提升了20%-30%。

闭包的解绑也是容易被忽略的点。闭包本身没问题,但如果在循环里创建闭包,每个闭包都会携带一份函数定义。在React/Vue这种组件化框架里尤其常见:render函数里每次执行都重新内联定义事件处理器。这不是"JS引擎太弱",而是每次渲染都重复创建函数对象,触发GC压力。做法上至少应该把不需依赖当前上下文的方法提取到组件外部,用缓存引用。

4. 从代码到大屏:DOM操作、事件和Canvas的性能战场

写完纯逻辑层面的优化,真正让用户感知到"流畅"或"卡顿"的,是浏览器渲染链路。这条链路上的三大性能杀手,分别是DOM操作、事件监听和Canvas渲染。

4.1 虚拟列表与DOM回收:别让浏览器背锅

中后台场景里最常见的性能事故,是把几千条数据一次性渲染成DOM节点。浏览器对DOM节点数没有硬性上限,但每多一个节点就多一份内存和布局计算成本。当一个列表有3000行时,打开Performance面板,你会看到Rendering时间随滚动直线上升。

这时候唯一正确的做法是虚拟列表——只渲染可视区内的条目。市面上成熟的方案有react-window、vue-virtual-scroller,但如果你只想快速解决一个阅读型列表,自己实现一个简易版也不难:

function VirtualList({ items, itemHeight, viewportHeight }) { const [start, setStart] = useState(0); const visibleCount = Math.ceil(viewportHeight / itemHeight) + 2; const visibleItems = items.slice(start, start + visibleCount); return ( <div style={{ height: viewportHeight, overflowY: 'auto' }} onScroll={handleScroll}> <div style={{ height: items.length * itemHeight, position: 'relative' }}> <div style={{ position: 'absolute', top: start * itemHeight }}> {visibleItems.map((item, i) => ( <Row key={start + i} data={item} height={itemHeight} /> ))} </div> </div> </div> ); }

核心逻辑就三件事:根据滚动位置算起始索引;渲染起始索引加上可视数量;用容器高度撑起滚动条。如果你用的是Vue,逻辑同理,换成computed就能跑通。

虚拟列表之外,还有一类隐含的DOM开销来自事件委托的反面——每个列表项都绑了一个监听器。5000个节点配5000个监听函数,光绑定就是一次不小的开销。我经手的一个项目里只改了这一条:在父容器上挂一个click监听,通过event.target.closest('[data-action]')找到具体操作项,页面交互从此变得跟德芙一样顺滑。

4.2 事件节流与防抖:两者别混着用

防抖(debounce)和节流(throttle)是高频事件处理的标准答案。但很多同学混着用,导致效果南辕北辙。

  • 防抖:多次触发只执行最后一次。适合输入联想、搜索请求这类"以最终结果为导向"的场景。
  • 节流:固定时间间隔内只执行一次。适合滚动、窗口缩放这类"需要连续反馈但不能太频繁"的场景。

具体到实现,防抖可以用setTimeout加清空来写:

function debounce(fn, delay = 300) { let timer = null; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; }

节流则常用时间戳版本:

function throttle(fn, interval = 200) { let last = 0; return function (...args) { const now = Date.now(); if (now - last >= interval) { last = now; fn.apply(this, args); } }; }

真正值得注意的坑不在实现本身,而在使用语境。移动端的touchmove事件,节流间隔设置为16ms(一帧的时间)以下就没意义了,因为事件的触发频率本来就低于帧率,再节流反而增加调度开销。而桌面端的mousemove,用100ms作为默认间隔体感最好。实践中我习惯先设一个值,打开FPS面板边测边调,而不是直接照着别人的配置抄。

4.3 Canvas渲染:别让每帧都重新画整张图

热搜词里"javascript canvas"热度不低,Canvas确实是数据可视化项目的核心战场。但很多人一旦发现Canvas卡,就以为是"Canvas性能不行"——实际上绝大多数卡顿出在"重复绘制"上。

常见错误是:在requestAnimationFrame回调里,每帧都把全部图形clearRect后重画一遍。如果图形数量很大(比如1万个节点),这种方式太伤性能。

优化的核心思路是分层绘制:

  • 静态层:把不变化的背景、网格、标签先画到一个离屏Canvas(OffscreenCanvas或普通canvas但不挂到DOM),每次只用drawImage把静态层贴回去。
  • 动态层:只重画运动的部分,比如拖拽中的节点、闪烁的标记。

代码层面大概是:

// 初始化:创建离屏Canvas承载静态内容 const staticCanvas = document.createElement('canvas'); staticCanvas.width = width; staticCanvas.height = height; const staticCtx = staticCanvas.getContext('2d'); // 在staticCtx上绘制网格、坐标轴等 // 动画循环中:先贴静态层,再绘制动态层 function frame() { ctx.clearRect(0, 0, width, height); ctx.drawImage(staticCanvas, 0, 0); drawDynamicNodes(ctx); // 只画需要变化的部分 requestAnimationFrame(frame); }

还有一个容易被忽视的Canvas性能点:canvas.width的赋值。即使赋一样的值,也会触发画布内容清空和上下文重置,尽量只在真正需要调整尺寸时才动它。另外,对高刷新率屏幕,requestAnimationFrame本身就是按屏幕刷新率执行的,不要人为再增加额外的定时器计数。

5. 移动端与跨浏览器兼容中的性能暗坑

移动端和跨浏览器兼容是热搜词里两个高频方向。很多开发者有一个惯性:把项目跑在Chrome上没问题就交付了。但真实的移动端环境里,事情远没那么简单。

5.1 事件触发延迟与触摸反馈

移动端浏览器有300ms点击延迟的历史包袱——双击缩放的判定机制造就的。现在主流浏览器在正确设置viewport后已自动移除延迟,但如果你遇到老版本WebView,还是可能撞上。这类场景下加一个touchstart事件的手动响应即可,但要注意touchstart无法触发click的默认行为链,需要在合适位置补一个click以防万一。

另一个现实问题是"感观性能"。即使在真机上JavaScript执行已经很快,触摸反馈不跟手,用户依然会说"卡"。做法是:触摸事件里只做状态更新和极简DOM变更,把复杂的计算和渲染延到requestAnimationFrame或宏任务里。其实这就是"先给用户反馈,再干累活"的思路,找对顺序往往比硬啃性能来得更快。

5.2 跨浏览器差异:优雅降级比Polyfill更划算

"跨浏览器兼容"这个词听起来很基础,但在性能优化的语境下,它意味着不要为了一小撮用户给所有用户背上包袱。

我见过一个项目,为了兼容一个老浏览器,全站引入了大体积Polyfill,解析执行时间多了将近200ms,换来的只是老浏览器上的一个不算关键的API。这时候正确的做法是评估访问量占比——如果老浏览器的访客比例不到1%,可以用特性检测做优雅降级,而不是给99%的用户加负担。

// 推荐:特性检测后按需加载 if ('IntersectionObserver' in window) { // 使用原生能力 } else { // 加载轻量降级方案,比如只对第一屏做懒加载 }

按需加载在代码层面要注意的,是怎么避免动态import在运行时产生阻塞。业界通用的做法是预加载和懒加载结合:首屏关键模块用预加载,次要模块滚动到附近再加载。Webpack/Vite都提供了webpackPreload和webpackPrefetch注释来精确控制加载时机。实测数据是,把非关键模块全部改成预取后,首屏可交互时间缩短了20%左右,代价仅是多了一些空闲时间的带宽消耗。

5.3 图片与资源传输:最大头的时间花在网络上

现在的JavaScript性能优化,如果只盯着代码不看资源体积,等于徒手救火。移动端尤其是:一张3MB的图片即使加载完成了,解码也是大笔开销。图片处理上我的建议很直接:

  • 使用WebP或AVIF格式(体积比JPEG小30%-50%)。
  • 使用响应式srcset按设备宽度加载不同分辨率。
  • 懒加载非首屏图片,用loading="lazy"即可,比自己监听滚动更可靠。
  • 对图标类小图,做成SVG雪碧图或直接内联,减少请求数。

打包层面,terser-webpack-plugin或者Vite内置的esbuild压缩已经是标配;再往前一步是代码分割到路由级,杜绝"进一个页面加载全部JS"的窘况。

6. 运行时报错与内存泄漏:性能问题的两大隐形杀手

热搜词里"javascript运行时报错"出现得很高频。报错和性能有什么关系?关系大了——一次未捕获的异常,会让当前任务直接中断,页面表现就是"突然卡一下"。而更隐蔽的,是那些不报错但默默泄漏内存的逻辑,日积月累导致页面越用越慢。

6.1 捕获异常的正确姿势:别让错误中断主流程

一个典型的坏例子是:在循环里调用了一个可能抛异常的解析函数,没有try/catch包裹。当第1000条数据解析失败时,整个循环中断,之前渲染的999条全部白做——用户等来的不是内容,是一个白屏。

// 推荐:隔离可能出错的任务 for (const item of items) { try { processItem(item); } catch (e) { // 记录错误,但保持循环继续 reportError(e, item); } }

这种"局部容错"的思路,在性能层面带来的收益往往比减少100行代码更明显,它防止的是"任务断层"带来的连锁性体验恶化。另一个容易被忽略的是异步任务里的异常。fetch请求返回的Promise.reject,如果没有catch处理,错误会冒泡到全局unhandledrejection事件。全局事件处理不会让页面崩溃,但会让开发者无法定位问题,等于埋下一颗延迟炸弹。我的习惯是:所有异步请求统一走一个请求封装,内部统一处理错误和超时,而不是在每个页面各写一套try/catch。

6.2 内存泄漏的四种惯犯

内存泄漏在单页应用里最典型,因为页面不刷新,泄漏只会累积,直到OOM崩溃。常见的四种泄漏源:

事件监听器:组件销毁时没移除window.addEventListener添加的监听。用原生API时千万记得在destroy或unmount钩子里移除;用框架时,框架自带的监听器大多会自动清理,但你自己添加的不会。

定时器:setInterval永远不会被GC回收,除非clearInterval。我在一个后台项目里抓出过一个这样的问题:每次打开弹窗都会创建一个3秒刷新一次的定时器,用户一天开20次弹窗,页面上挂了20个定时器,FPS直接被拖死。

闭包引用:闭包把大对象引用牢牢攥在手里。典型场景是:把一个体积很大的数据对象定义在函数外部,内部函数只需要其中某几个字段,但闭包却把整个对象吞了进去。解决方式是只传入需要的字段,别为大对象创建长期存活的高层引用。

Detached DOM节点:JavaScript变量还持有DOM引用,但节点已经从文档中移除了。排查方法很简单:在Chrome的Heap Snapshot里搜索"Detached",看谁还在引用它们。

定位泄漏的方式,上面提到过用Memory面板做堆快照对比:先录一次快照,执行一个操作,再录一次快照,反复几次之后对比哪些对象数量在稳定增长。这个方法虽然笨,但非常可靠,比猜代码高效百倍。

6.3 减少GC压力也是一种优化

JavaScript的垃圾回收(GC)机制是自动的,但GC执行时会"暂停"JavaScript运行,叫做STW(Stop The World)。频繁创建大量短生命周期对象,GC就会频繁触发,页面就表现为"间歇性卡顿"。

优化思路大致分两类:一类是对象复用。比如在动画循环里,避免每帧都创建新的坐标对象,而是初始化一个池子反复用:

// 推荐:对象池复用 const pointPool = []; function getPoint(x, y) { let point = pointPool.pop() || { x: 0, y: 0 }; point.x = x; point.y = y; return point; } function releasePoint(point) { pointPool.push(point); }

另一类是减少临时字符串。比如一次性输出大段HTML时,用数组join而不是反复+=(虽然现代引擎优化了,但并行循环里依然存在差异),以及尽量避免在热路径上调用JSON.stringify和JSON.parse。

顺带提一句热搜里出现的"julia性能优化与内存管理"和"oracle sql性能优化"——这两个虽然语言栈不同,但底层思路本质一样:减少不必要的分配,控制对象的生命周期。性能优化在很多领域都是相通的,理解了JavaScript这一套,跨语言看问题也不会太陌生。

7. 一个真实案例:从"白屏三秒"到"秒开"的完整过程

光讲理论和零散技巧,可能还是感觉没落地。我拿去年处理的一个项目复盘一下,把前面的知识点串成一条完整的链路。

项目背景是一个移动端的数据报表页面,清单页每次进入要加载近2000条数据,每条数据有十几个字段,前端拿到后要经过过滤、排序、格式化再渲染。症状是:白屏时间约3秒,滚动明显掉帧,iOS上尤其严重。

7.1 体检定位:问题不在接口

第一步先量化。Performance面板录制进入页面到渲染完成的过程,结果发现最长的阻塞不在网络请求——接口耗时约500ms,但JSON解析加数据处理加首次渲染,花了近2.4秒。

紧接着用Memory面板做堆快照,发现一次进入页面就创建了组件实例和事件监听器一大堆对象。第三步用PerformanceObserver监听长任务,抓到了3个超过500ms的长任务,全部集中在数据处理阶段。

7.2 三管齐下的修复

针对抓到的三个问题,我做了三件事:

数据层面:后端加了一个summary接口,列表页只传展示用字段(缩减了对全部字段处理的开销),并把前置的过滤排序交给后端。前端只处理2000条精简数据,主线程压力骤降。

渲染层面:把列表换成虚拟滚动,只渲染可见的15条。同时把每条数据计算出来的格式化结果做缓存,因为用户翻回去时会重复渲染同一个item——有缓存之后,滚动回弹开箱即用。

内存层面:把所有页面级事件监听统一收口到页面控制器里,页面销毁时统一解绑。修复了一处长列表item组件里对全局store的闭包引用。

7.3 优化后的数据对比

加上不科学的"实测体感"前,先看数据:

指标优化前优化后
可交互时间约3.0s约0.9s
长任务数量3个(均超500ms)0个
FPS(滚动时)18-2555-60
页面内存(稳定后)84MB31MB

这个结果不算极端,但足以说明问题:只要找准了瓶颈,三管齐下后页面性能会有一个质的飞跃。整个优化过程花了两天时间,其中半天在定位,一天半在改。真正干的活不算多,但定位的功夫占了大部分。

7.4 一劳永逸?不,还得防回潮

优化完回归测试通过之后,我顺手加了两个防回潮的机制。一个是自己写的简易性能预算脚本:在CI阶段跑一次Lighthouse,如果LCP超过1.5秒或者总包体超过某个阈值,就直接让构建失败。另一个是在关键页面埋了性能上报,用performance.mark+measure把可交互时间和长任务数量发到监控平台。这样一来,即便日后有人改了代码导致性能劣化,也不会等到线上被用户反馈才发现。

8. 高频排查清单与工具组合

基于前面这些实战,整理一份可以直接照着做的排查清单。我在项目里也是按这个顺序过一遍的,能覆盖90%的性能问题。

  1. 记录基线:用Performance面板录一段典型交互,记录FPS、Scripting占比、Rendering占比。
  2. 监听长任务:用PerformanceObserver把长任务列出来,耗时从高到低排队处理。
  3. 检查网络瀑布流:看有没有大体积堵车资源、多余的请求、缓存没生效的接口。
  4. 审查JS包体:用webpack-bundle-analyzer或Vite的rollup-plugin-visualizer看各模块体积。
  5. 检查DOM规模:打开Elements面板看总节点数,超过3000就要考虑虚拟列表或简化DOM结构。
  6. 排查内存泄漏:Memory面板连续做三次操作-快照对比,找增长的对象。
  7. 验证第三方库成本:把库从代码里临时注释掉,看性能指标发生多大变化,高成本低收益的直接换。

工具组合方面,官方三件套DevTools、Lighthouse、PageSpeed Insights是我用得最多的。Lighthouse侧重综合评分,PageSpeed Insights拿的是真实用户场景的Field Data,DevTools则负责一切底层细节。另外不要忽视react-devtools或vue-devtools的Profiler,组件层面到底哪一步渲染耗时最长,这里看得最清楚。

9. 两个我自己反复踩过的坑,提前帮你们趟了

最后说两个不太容易在网络文章里找到答案的私房经验。

第一个是关于React/Vue列表key的"最佳实践"争议。很多人知道不要用索引当key,但用随机ID当key在长列表里也会出问题:每次数据更新,所有item的key都变了,框架只能全部销毁重建,这比用索引的代价大多了。正确做法是:稳定的、可复用的、与业务数据绑定的key才是最优解;如果数据本身没有天然唯一ID,用索引反而比随机ID强。这条经验靠背诵学不来,得在真实长列表上踩过一遍才记得住。

第二个是关于**"清理定时器和监听器"的时机**。不是"页面销毁时清理"就完事了,有些场景需要在"页面不可见时暂停、重新可见时恢复"。比如地图可视化页面,切到后台后定时轮询接口还在跑,刷后台数据是纯浪费网络和CPU。用document.visibilitychange事件,在document.hidden为true时暂停所有空闲任务,回来再恢复,对移动端尤其是iPhone这种资源受限场景收益巨大。

我把这两种坑在前面项目复盘中也都补上了——给每个item沿用后端返回的稳定ID,地图页切走后自动暂停轮询。这些细节看上去不起眼,但往往是"优化完之后怎么还是有点卡"的最后一块拼图。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询