批量任务导致页面卡死解决方案

news2024/12/21 16:34:22

需求背景

        需要基于高德地图展示海量点位(大概几万个),点位样式要自定义(创建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 = innerHTML
    marker.setContent(container)
    marker.setOffset(markerOffset)
    marker.selected = false

    return 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 something
      console.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 = 0
  let 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 = 0
  return 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.port1
  const port2 = channel.port2
  let deadlineTime: number // 超时时间
  let frameDeadlineTime: number // 当前帧的截止时间
  let callback: (idleDeadline: IdleDeadline) => void

  const 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.7
        port1.postMessage(null)
      })
    }
  }

  idMap[id] = window.requestAnimationFrame((timeStamp) => {
    frameDeadlineTime = timeStamp + 16.7 // 当前帧截止时间,按照 60fps 计算
    deadlineTime = options?.timeout ? timeStamp + options.timeout : Infinity // 超时时间
    callback = cb
    port1.postMessage(null)
  })

  return id
}

const _cancelIdleCallback = function (id: number) {
  if (!idMap[id]) return
  window.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 } = this
    let index = 0
    const 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)
}

        可以看到,点位的渲染并没有再影响到页面的响应了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/505233.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

为什么剑桥出身的“AI教父”辛顿会担心?

剑桥很有意思&#xff01;在那儿呆过的人常常有这样一种感觉&#xff1a;剑&#xff0c;很锋利&#xff01;桥&#xff0c;很温柔&#xff01;剑桥的科技自不必说&#xff0c;牛顿、达尔文、麦克斯韦、爱丁顿......&#xff0c;剑桥的人文却也不让科技&#xff0c;拜伦、培根、…

使用asp.net core web api创建web后台,并连接和使用Sql Server数据库

前言&#xff1a;因为要写一个安卓端app&#xff0c;实现从服务器中获取电影数据&#xff0c;所以需要搭建服务端代码&#xff0c;之前学过C#&#xff0c;所以想用C#实现服务器段代码用于测试&#xff0c;本文使用C#语言&#xff0c;使用asp.net core web api组件搭建服务器端&…

【软考备战·希赛网每日一练】2023年5月9日

文章目录 一、今日成绩二、错题总结第一题三、知识查缺 题目及解析来源&#xff1a;2023年05月09日软件设计师每日一练 一、今日成绩 二、错题总结 第一题 解析&#xff1a; 有损、无损连接判断&#xff1a; (A1,A2)∩(A1,A3)A1 (A1,A2)-(A1,A3)A2 (A1,A3)-(A1,A2)A3 所以A1-&…

车载5G放量增长,哪些厂商抢跑

前装标配19.88万辆&#xff0c;同比上年同期增长724.89%&#xff0c;这是一季度中国市场乘用车5G搭载上车交付的成绩。高工智能汽车研究院监测数据显示&#xff0c;2022年全年5G交付搭载41.74万辆&#xff08;不含选装&#xff09;&#xff0c;前装搭载率为2.09%。 这意味着&a…

【Linux】GDB多进程调试

目录 GDB多进程调试 GDB多进程调试 演示父子进程如何进行gdb调试会用到hello.c文件 hello.c文件内容如下&#xff1a; #include <stdio.h> #include <unistd.h>int main() {printf("begin\n");if(fork() > 0) {printf("我是父进程&#xff1…

Baumer工业相机堡盟工业相机如何使用BGAPI SDK进行两个万兆网相机的同步采集

Baumer工业相机堡盟工业相机如何使用BGAPI SDK进行两个万兆网相机的同步采集 Baumer工业相机Baumer工业相机图像数据转为Bitmap的技术背景Baumer同步异常 &#xff1a;客户使用两个Baumer万兆网相机进行同步采集发现FrameID相同&#xff0c;但是图像不同步细节原因解决办法 Bau…

Windows下python中的pip换源

在Windows中更换pip数据源方法&#xff0c;提高Python相关包安装效率 1.在windows环境下&#xff0c;打开我的电脑&#xff0c;在"地址栏"输杰沫入: %APPDATA% 后回车 2.在打开的文件夹中新建 pip 文件夹&#xff08;打开的地址为下图所示&#xff09; 3.进入pip文…

在 Python 中将泊松分布拟合到不同的数据集

文章目录 在 Python 中将泊松分布拟合到不同的数据集在 Python 中拟合泊松分布的分箱最小二乘法程序的导入函数为泊松分布创建一个虚拟数据集并使用该数据集绘制直方图使用曲线拟合将曲线拟合到直方图 使用负二项式拟合过度分散的数据集上的泊松分布创建数据集使用数据集绘制直…

JAVA算法(一)查找算法

一、基本查找 / 顺序查找 核心&#xff1a;从0索引开始挨个往后查找 private static boolean basicSearch(int[] arr, int number) {for (int i 0; i < arr.length; i) {if (arr[i] number) {return true;}}return false;}二、二分查找 / 折半查找 前提&#xff1a;数组…

(四) 打造更加智能的即时通信系统——实现主界面消息和联系人切换效果

文章目录 一、引言二、界面设计的基本要求2.1 界面美观简洁2.2 功能合理布局 三、界面布局和控件设计四、效果展示五、关键代码六、个人经验分享6.1 即时通信系统开发中的经验和总结6.2 遇到的问题和解决方案6.3优化即时通信系统 七、总结 一、引言 当今社会&#xff0c;人们对…

解决 scalac: bad option: ‘-make:transitive‘

scalac: bad option: ‘-make:transitive’ 打开项目所在位置并进入 .idea 修改scala_compiler.xml文件 删除掉参数行包含-make:transitive 保存后 重新运行代码

Linux驱动开发笔记(二):ubuntu系统从源码编译安装gcc7.3.0编译器

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/130533941 红胖子网络科技博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬…

uniapp云打包

1. 生成本地打包资源 2. 下载Android Studio和App离线SDK 导航路径&#xff1a;https://nativesupport.dcloud.net.cn/AppDocs/usesdk/android 下载的Android 离线SDK与本地开发工具版本对应 3. 解压得到的目录如下 4. 使用Android Studio打开Android 离线SDK解压目录里面这个…

计算机毕业论文选题推荐|软件工程|系列一

文章目录 导文题目导文 计算机毕业论文选题推荐|软件工程 (***语言)==使用其他任何编程语言 例如:基于(***语言)门窗账务管理系统的设计与实现 得到:基于JAVA门窗账务管理系统的设计与实现 基于vue门窗账务管理系统的设计与实现 等等 题目 基于(***语言)的研发部署管理…

全方位揭秘!大数据从0到1的完美落地之MapReduce实战案例(1)

案例一: MR实战之小文件合并(自定义inputFormat) 项目准备 需求 无论hdfs还是MapReduce&#xff0c;对于小文件都有损效率&#xff0c;实践中&#xff0c;又难免面临处理大量小文件的场景&#xff0c;此时&#xff0c;就需要有相应解决方案 测试数据 分析 小文件的优化无…

ClickHouse:联接执行的内部机制

在之前的文章中&#xff0c;我们回顾了 ClickHouse 中可用的 SQL JOIN 类型。提醒一下&#xff1a;ClickHouse 附带完整的 SQL 连接支持。 在本文中&#xff0c;我们将探索 ClickHouse 中联接执行的内部结构&#xff0c;以便您可以优化应用程序使用的查询的联接。在这里&#…

数据集进行拆分到底什么样数据算是数据标签什么样的数据算数据样本

点击上方“Python爬虫与数据挖掘”&#xff0c;进行关注 回复“书籍”即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 忽闻海上有仙山&#xff0c;山在虚无缥缈间。 大家好&#xff0c;我是皮皮。 一、前言 前几天在Python白银群【kim】问了一个Python机器学习的问题&…

【AI大模型】SparkDesk讯飞星火认知大模型初体验-国内最强ChatGPT

文章目录 前言SparkDesk讯飞星火认知大模型简介语言理解知识问答逻辑推理数学题解答代码理解与编写亲自体验写在最后 前言 5月6日&#xff0c;讯飞星火认知大模型成果发布会在安徽合肥举行。科大讯飞董事长刘庆峰、研究院院长刘聪发布讯飞星火认知大模型&#xff0c;现场实测大…

5. 类和对象

一、面向对象程序设计的基本特点 1.1 抽象 指对具体问题&#xff08;对象&#xff09;进行概括&#xff0c;抽出一类对象的公共性质并加以描述的过程 数据抽象 描述某类对象的属性或状态&#xff0c;即此类对象与其他类对象的区别 行为抽象 描述某类对象的共同行为或功能特征…

计算机毕业论文内容参考|基于神经网络的网络安全态势感知技术研究

文章目录 导文文章重点摘要前言绪论课题背景国内外现状与趋势课题内容相关技术与方法介绍技术分析技术设计技术实现总结与展望导文 基于神经网络的网络安全态势感知技术研究 文章重点 摘要 随着互联网的快速发展,网络攻击的频率和复杂度也在逐年增加。为了更好地保护信息系统…