批量任务导致页面卡死解决方案
需求背景
需要基于高德地图展示海量点位(大概几万个),点位样式要自定义(创建DOM),虽然使用了聚合点,但初始化时仍需要将几万个点位的DOM结构都创建出来。
这里补充一句,高德地图在2.0版本对这种方式进行了优化,但同时少了某些功能,我的需求要使用1.4版本的这种方式渲染。
问题及定位分析
功能实现后,发现从开始加载点位,到点位出现的过程中,页面会卡死,无法响应用户交互,可以点击Demo的常规模式查看效果(实际业务下有更多逻辑,阻塞时间会更久)。

可以看到,当我开始渲染点位后,点击输入框进行输入,是没有立即响应的,点位加载完后才会对之前的交互做响应。
问题分析
其实从上面高德地图的点位渲染逻辑很容易想到主要是批量创建点位的DOM结构占用了主线程

可以看到,批量的genMarker任务占用了大量时间,genMarker会在每次创建点位时执行一次,一次创建4w个点位,就会连续执行4w次。
// 生成点位,创建DOM自定义样式
genMarker(device) {const innerHTML = `<div class="camera"></div>`const size = [48, 49]const markerOffset = new AMap.Pixel(-size[0] / 2, -size[1] / 2)const marker = new AMap.Marker({position: device.lnglat,extData: device,size,})const container = document.createElement('div')container.className = 'map-marker'container.innerHTML = innerHTMLmarker.setContent(container)marker.setOffset(markerOffset)marker.selected = falsereturn marker
}
页面显示机制
动的画面其实是由一帧一帧的静态图快速切换组成的,人眼的反应速度有限,当画面切换的够快,人眼看着就是连续的动画了。
对于人眼来说,当每秒切换60张图片时,就会认为是连贯的。所以主流的显示器是60hz的,1s刷新60次,那么每16.7ms需要刷新一次,浏览器会自动适配这个频率,这时对应我们前端页面就是每16.7ms需要渲染一次。

页面每隔16.7ms才会渲染一次,那么在两次渲染的中间时间,就是浏览器的空闲时间,在这段空闲时间执行的任务,是不会阻塞到页面渲染的流畅性的。反之,对于上面的案例,数万个genMarker在一个帧区间内连续的执行,下一帧一直不能渲染,页面看起来就被卡住了。

任务拆分
对于大量的计算或许首先考虑的是Web Worker使其不占用主线程,但是由于要操作DOM,不适合当前场景。
对于页面的流畅性来说,这些点位的创建属于「低优先级任务」。既然卡顿的原因是这些genMarker任务一个接一个的「连续」的在执行,一直占用着主线程,那么我们可以将这些批量的任务进行拆分,保证这些任务只在空闲时间执行。每次执行下一个任务的时候,先检查一下当前页面是否该渲染下一帧了,这时需要「把主线程让出来」,让页面进行渲染(了解react的人应该感觉很熟悉,思路来自react的Fiber)

requestIdleCallback
「让出主线程」,关键的一点在于我们如何知道什么时候是空闲时间,什么时候空闲时间结束,该进行渲染了。requestIdleCallback就是浏览器提供给我们用来判断这个时机的api,它会在浏览器的空闲时间来执行传给它的回调函数。另外如果指定了超时时间,会在超时后的下一帧强制执行
const id = window.requestIdleCallback((deadline) => {// 当前帧剩余时间大于0,或任务已超时if(deadline.timeRemaining() > 0 || deadline.didTimeout) {// do somethingconsole.log(1)}
}, { timeout: 2000 }) // 指定超时时间// window.cancelIdleCallback(id) 与定时器类似,支持取消
requestIdleCallback在Event Loop的执行时机如下图所示,蓝色区域代表一帧内的渲染任务,当这些任务执行完后,剩余的时间被认为是空闲时间

以一个简单的任务(singlTask)为例,以常规模式连续执行2w次,全部执行完需要大概2s时间(依赖机器性能变化),这期间主线程被一直被占用,页面会被卡住。
function singleTask() {const now = performance.now()while (performance.now() - now < 0.001) { } // 模拟耗时操作,每次任务耗时约0.001ms
}const data = new Array(20000).fill(1)function normarlRun() {for (let i = 0; i < data.length; i++) {// 2w个任务连续执行singleTask(data[i])}result('done')
}
对其使用requestIdleCallback进行拆分,只在空闲时间执行部分任务,若当前帧的空闲时间结束,则暂停批量任务,让出主线程:
function ridRun() {let i = 0let option = { timeout: 200 } // 任务超时时间function handler(idleDeadline) {while ((idleDeadline.timeRemaining() > 0 || idleDeadline.didTimeout) && i < data.length) {// 当前帧有剩余时间,或任务已等待超时强制执行singleTask(data[i++])}// idleDeadline.timeRemaining() === 0 当前帧已没有空闲时间,让出主线程if (i < data.length) {window.requestIdleCallback(handler, option) // 任务未执行完,继续等待下次空闲时间执行} else {result('done')}}window.requestIdleCallback(handler, option)
}
模拟requestIdleCallback
不幸的是requestIdleCallback兼容性不够好,Safari完全不支持:
参考react的实现,我们可以使用requestAnimationFrame和MessageChannel来模拟实现一个requestIdleCallback。requestAnimationFrame在每一帧开始渲染前执行(见上面的Event Loopt图),当帧开始渲染前,我们标记开始时间(start),并使用MessageChannel创建一个宏任务,根据上面的Event Loop流程,渲染完毕后,会执行刚才创建出的宏任务,这时在宏任务中对比标记的开始时间,是否超出了一帧的渲染时间(current - start > 16.7),来判断当前是否是空闲时间。
setTimeout即使指定时间为0 浏览器实际也会延时几毫秒后才执行(chrome大概为4ms),因此使用MessageChannel而不是setTimeout来创建宏任务
模拟requestIdleCallback的具体实现:
const genId = (function () {let id = 0return function () {return ++id}
})()const idMap: {[key: number]: number
} = {}const _requestIdleCallback: (cb: (idleDeadline: IdleDeadline) => void,options?: { timeout: number }
) => number = function (cb, options) {const channel = new MessageChannel()const port1 = channel.port1const port2 = channel.port2let deadlineTime: number // 超时时间let frameDeadlineTime: number // 当前帧的截止时间let callback: (idleDeadline: IdleDeadline) => voidconst id = genId()port2.onmessage = () => {const frameTimeRemaining = () => frameDeadlineTime - performance.now() // 获取当前帧剩余时间const didTimeout = performance.now() >= deadlineTime // 是否超时if (didTimeout || frameTimeRemaining() > 0) {const idleDeadline = {timeRemaining: frameTimeRemaining,didTimeout}callback && callback(idleDeadline)} else {idMap[id] = requestAnimationFrame((timeStamp) => {frameDeadlineTime = timeStamp + 16.7port1.postMessage(null)})}}idMap[id] = window.requestAnimationFrame((timeStamp) => {frameDeadlineTime = timeStamp + 16.7 // 当前帧截止时间,按照 60fps 计算deadlineTime = options?.timeout ? timeStamp + options.timeout : Infinity // 超时时间callback = cbport1.postMessage(null)})return id
}const _cancelIdleCallback = function (id: number) {if (!idMap[id]) returnwindow.cancelAnimationFrame(idMap[id])delete idMap[id]
}export const requestIdleCallback = window.requestIdleCallback || _requestIdleCallback
export const cancelIdleCallback = window.cancelIdleCallback || _cancelIdleCallback
使用requestIdleCallback拆分点位生成
将genMarker批量任务进行拆分,只在空闲时间时间进行拆分:
addMarkersByRid() {cancelIdleCallback(this.ridId)const { markerList, points, genMarker, genCluster } = thislet index = 0const ridOption = { timeout: 20 }const handler = (idleDeadline) => {const { timeRemaining } = idleDeadline// 只在空闲时间生成点位while (timeRemaining() > 0 && index < points.length) {const device = points[index]const marker = genMarker(device)markerList.push(marker)index++}if (index < points.length) {this.ridId = requestIdleCallback(handler, ridOption)} else {console.log('done') // 全部点位生成完毕}}this.ridId = requestIdleCallback(handler, ridOption)
}

可以看到,点位的渲染并没有再影响到页面的响应了
相关文章:
批量任务导致页面卡死解决方案
需求背景 需要基于高德地图展示海量点位(大概几万个),点位样式要自定义(创建DOM),虽然使用了聚合点,但初始化时仍需要将几万个点位的DOM结构都创建出来。 这里补充一句,高德地图在2.…...
避免“文献综抄”,5种写作结构助你完成文献综述→
很多作者可能有过这样的体验:读了很多文献,但在写综述的时候总感觉不像是在写文献综述,更像在写文献总结 如果引用方面不注意,甚至会成为文献综抄。 那么,你可以参考下我们整理的以下资料哦~ 01 文献总结和文献综述的…...
Java异常和反射
JAVA 异常分类及处理 概念 } final Entry<K,V> getEntryUsingComparator(Object key) { K k (K) key; // 获取该 TreeMap 的 comparator Comparator<? super K> cpr comparator; if (cpr ! null) { // 从根节点开始 Entry<K,V> p …...
Accesss数据库的那点事
Accesss数据库的那点事 1.Access的简介 Access(全称为Microsoft Access)是一个关系型数据库管理系统(RDBMS)。它是由微软公司开发的数据库软件,用于创建、管理和操作数据库应用程序。 Access提供了一个可视化的开发环…...
网络基础学习:osi网络七层模型
osi网络七层模型 什么是OSI,什么是ISO?为什么ISO要提出OSI网络七层模型?OSI七层的划分以及具体内容第七层 应用层第六层 表示层第五层 会话层第四层 传输层第三层 网络层第二层 数据链路层第一层 物理层 每一层与设备的对应关系 什么是OSI,什…...
EndNote X9 引用参考 单击文献编号,不能跳转到文尾文献列表处,咋解决?文献编号 不能跳转 ,怎么办?
文章目录 1 正常情况下 引用文献编号 是可以跳转的2 问题分析3 解决方法4 EndNote X9 插入参考文献常见问题总结5 EndNote X9 快速上手教程(毕业论文参考文献管理器) 1 正常情况下 引用文献编号 是可以跳转的 正确的插入文献后, 正常情况下&a…...
用免费蜜罐工具配置Modbus工控蜜罐
导语:本文将用DecoyMini免费蜜罐工具来配置自定义的ModbusTCP工控仿真模板,并介绍部署后的Modbus蜜罐的使用效果。 DecoyMini是一个免费的蜜罐工具,其特色是仿真能力采用与软件松耦合的仿真模板来进行管理。通过一键式导入云端仿真模板库里的…...
DataGridXL中快速搜索单元格和底部全屏模式区域隐藏
DataGridXL表格是在2020年发布,DataGridXL在设计时就考虑到了性能。提供最快、最简单、最可靠的数据网格。DataGridXL支持所有常用所有的浏览器,为 Web 应用程序提供类似于 Microsoft Excel 的体验,它支持前端框架有Vue、React、Angular等。 …...
DotNet几种微服务框架,你用过吗?
最近有群友问,.NET有哪些微服务框架?.NET的微服务框架还真不多,一般企业都会自己搭建微服务框架,或者基于其它框架搭建微服务(比如abp)。本文将介绍几种微服务框架,供大家学习参考。 一、Servi…...
Nature | 生成式人工智能如何构建更好的抗体
疫情高峰期,研究人员竞相开发一些首批有效的COVID-19治疗方法:从已经康复的人的血液中分离出来的抗体分子。 现在,科学家已经证明,生成式人工智能(AI)可以通过一些繁琐的过程提供捷径,提出增强抗…...
【hive】基于Qt5和libuv udp 的lan chat
作者已经不更新了,但是很棒 在线用户列表: 聊天窗口 主程序 单独的网络线程: network_thread data管理关联网络管理的 程序update升级更新 和消息收到 即可...
Java版本工程项目管理系统源码,助力工程企业实现数字化管理
Java版工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离 功能清单如下: 首页 工作台:待办工作、消息通知、预警信息,点击可进入相应的列表 项目进度图表:选择(总体或单个)项目显示…...
什么是零拷贝?
零拷贝 什么是零拷贝 零拷贝指的是,从一个存储区域到另一个存储区域的copy任务无需CPU参与就可完成。零拷贝的底层是 通过DMA总线技术实现的。零拷贝与具体的编程语言无关,完全依赖于OS,OS支持就可使用,不支持 设置了也不起作用…...
计算机专业含金量高的证书
目录 第一种证书:计算机技术与软件专业资格考试证书 第二种证书:微软认证 第三种证书:Oracle认证 第四种证书:思科认证 第五种证书:华为认证 第六种证书:红帽认证工程师 第七种证书:阿里…...
原装二手Keithley 2401低压源表 吉时利2401数字源表
Keithley 2401低压源表,20V,1A,20W Keithley 2401 低压源表提供精密电压和电流源和测量功能(1V - 20V 和 10pA - 1A)。它既是高度稳定的直流电源,又是真正的仪器级 5 位万用表。电源特性包括低噪声、精度和…...
gradle-8.1.1-all 快速下载百度网盘下载
Gradle 8.1.1 发布 这是Gradle 8.1的第一个补丁发布。 它修复了以下3个问题: 为配置缓存检测具有数千个lambdas的类时出现MethodTooLargeException;用Gradle 8.1构建的Kotlin DSL预编译脚本插件不能用于其他版本的Gradle;Gradle 8.1在buil…...
C#开发的OpenRA游戏之基地工程车部署命令产生过程
C#开发的OpenRA游戏之基地工程车部署命令产生过程 OpenRA游戏里,前面已经分析基地工程车部署的流程, 但那只是一个框架的流程,只能理解大体的框架, 如果想要深入一步了解怎么样产生部署命令,还需继续探讨以下的代码。 基地工程车是先选中,然后再在基地工程车上面点击右…...
C++ 智能指针的原理、分类、使用
1. 智能指针介绍 为解决裸指针可能导致的内存泄漏问题。如: a)忘记释放内存; b)程序提前退出导致资源释放代码未执行到。 就出现了智能指针,能够做到资源的自动释放。 2. 智能指针的原理和简单实现 2.1 智能指针的原…...
学习笔记——SVG.js中形状元素的创建及其相关方法
CreateElement 1)创建svg元素 在svg.js中,每个元素都是一个对象,可以通过构造它来创建: import { Rect } from "svgdotjs/svg.js" var rect new Rect().size(100, 100).addTo(draw) // or to reuse an existing nod…...
Linux一学就会——系统文件I/O
Linux一学就会——系统文件I/O 有几种输出信息到显示器的方式 #include <stdio.h> #include <string.h> int main() {const char *msg "hello fwrite\n";fwrite(msg, strlen(msg), 1, stdout);printf("hello printf\n");fprintf(stdout, &q…...
【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型
摘要 拍照搜题系统采用“三层管道(多模态 OCR → 语义检索 → 答案渲染)、两级检索(倒排 BM25 向量 HNSW)并以大语言模型兜底”的整体框架: 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后,分别用…...
国防科技大学计算机基础课程笔记02信息编码
1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制,因此这个了16进制的数据既可以翻译成为这个机器码,也可以翻译成为这个国标码,所以这个时候很容易会出现这个歧义的情况; 因此,我们的这个国…...
conda相比python好处
Conda 作为 Python 的环境和包管理工具,相比原生 Python 生态(如 pip 虚拟环境)有许多独特优势,尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处: 一、一站式环境管理:…...
7.4.分块查找
一.分块查找的算法思想: 1.实例: 以上述图片的顺序表为例, 该顺序表的数据元素从整体来看是乱序的,但如果把这些数据元素分成一块一块的小区间, 第一个区间[0,1]索引上的数据元素都是小于等于10的, 第二…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...
Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理
引言 Bitmap(位图)是Android应用内存占用的“头号杀手”。一张1080P(1920x1080)的图片以ARGB_8888格式加载时,内存占用高达8MB(192010804字节)。据统计,超过60%的应用OOM崩溃与Bitm…...
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材)
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材) 这个项目能干嘛? 使用 gemini 2.0 的 api 和 google 其他的 api 来做衍生处理 简化和优化了文生图和图生图的行为(我的最主要) 并且有一些目标检测和切割(我用不到) 视频和 imagefx 因为没 a…...
Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storms…...
wpf在image控件上快速显示内存图像
wpf在image控件上快速显示内存图像https://www.cnblogs.com/haodafeng/p/10431387.html 如果你在寻找能够快速在image控件刷新大图像(比如分辨率3000*3000的图像)的办法,尤其是想把内存中的裸数据(只有图像的数据,不包…...
tomcat指定使用的jdk版本
说明 有时候需要对tomcat配置指定的jdk版本号,此时,我们可以通过以下方式进行配置 设置方式 找到tomcat的bin目录中的setclasspath.bat。如果是linux系统则是setclasspath.sh set JAVA_HOMEC:\Program Files\Java\jdk8 set JRE_HOMEC:\Program Files…...
