惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

SecWiki News
SecWiki News
I
InfoQ
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
博客园 - Franky
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园_首页
罗磊的独立博客
V
V2EX
李成银的技术随笔
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
True Tiger Recordings
Vercel News
Vercel News
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
F
Fox-IT International blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
M
Microsoft Research Blog - Microsoft Research
Know Your Adversary
Know Your Adversary
爱范儿
爱范儿
The Register - Security
The Register - Security
G
Google Developers Blog
The Hacker News
The Hacker News
Malwarebytes
Malwarebytes
S
Securelist
博客园 - 三生石上(FineUI控件)
Jina AI
Jina AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
博客园 - 叶小钗
F
Fortinet All Blogs
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 聂微东
T
Threatpost
博客园 - 【当耐特】
D
Docker
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V
Visual Studio Blog
C
Cisco Blogs
IT之家
IT之家
S
Security Archives - TechRepublic
Latest news
Latest news
阮一峰的网络日志
阮一峰的网络日志

元视角

Terraform 极简入门:从 AWS-CLI 到基础设施即代码(IaC) - 元视角 .NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 盛世幻象:从荔枝看盛唐的兴衰 - 元视角 观影 - 元视角 听歌 - 元视角 个人作品 - 元视角 站点统计 - 元视角 读书 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 留言 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 命运、偏见与自由:《魔童之哪吒闹海》的终极抗争 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 走走停停,允许一切发生 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 从抖音看见世界的参差 - 元视角 俯仰之间:五一小长假出行记 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 《你想活出怎样的人生》与宫崎骏的自我和解 - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 你好,千寻小姐 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 杂感·七月寄望 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 在 Vue.js 中使用 Mock.js 实现接口模拟 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 读《一个叫欧维的男人决定去死》 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 烟波梦影,从天国王朝到刺客信条 - 元视角 关于 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 写在冬阳升起以前 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 从《失控玩家》中得到的启示 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 夕雾花园:从建筑中读出的爱情和美学 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 洗衣随想曲 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 浪客剑心:一曲幕末时代的挽歌 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的负载均衡 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 假如时间有温度 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角
百度地图加载海量标注性能优化策略 - 元视角
2019-09-10 · via 元视角

在上一篇博客中关于 Vue 表单验证的话题里,我提到了这段时间在做的城市配载功能,这个功能主要着眼于,如何为客户提供一条路线最优、时效最短、装载率最高的路线。事实上,这是目前物流运输行业智能化、专业化的一个趋势,即面向特定行业的局部最优解问题,简单来说,怎么样能在装更多货物的同时满足运费更低的条件,同时要考虑超载等等不可抗性因素,所以,这实际上是一个数学问题。而作为这个功能本身,在地图上加载大量标注更是基础中的基础,所以,今天这篇博客想说说,通过百度地图 API 加载海量标注时,关于性能优化方面的一点点经验。

问题还原

根据 IP 定位至用户所在城市后,后台一次性查询出近一个月内的订单,然后将其全部在地图上展示出来。当用户点击或者框选标注物时,对应的订单配载到当前运单中。当用户再次点击标注物,则对应的订单从当前运单中删除。以西安市为例,一次性加载 850 个左右的订单,用户操作一段时间后,Chrome 内存占用达 250 多兆,拖拽地图的过程中可以明显地感觉到页面卡顿。因为自始至终,地图上的订单数量不变,即不会移除覆盖物,同时需要在内存中持久化订单相关的信息。所以,在城市配载 1.0 版本的时候,测试同事给我提了一个性能方面的 Bug。可开始提方案并坚持这样做的,难道不是产品吗?为什么要给开发提 Bug 呢?OK,我们来给不靠谱的产品一点点填坑吧,大概想到了下面三种方案,分别是标注物聚合 、Canvas API 和视野内可见。

密密麻麻的地图 密密麻麻的地图

标注物聚合方案

所谓“标注物聚合”,就是指在一定的地图层级上,地图上的覆盖物主要是以聚合的形式显示的,譬如显示某一个省份里共有多少个订单,而不是把所有订单都展示出来,除非地图放大到一定的层级。这种其实在我们产品上是有应用的,比如运单可视化基本上是全国范围内的车辆位置,这个时候在省一级缩放比例上使用聚合展示就非常有必要。可在城市配载这里就相当尴尬啦,因为据说客户会把地图放大到市区街道这种程度来对订单进行配载,所以,这种标注物聚合方案的效果简直是微乎其微,而且更尴尬的问题在于,官方的 MarkerClusterer 插件支持的是标准的覆盖物,即 Marker 类。而我们的产品为了好看、做更复杂的交互,设计了更复杂的标记物原型,这就迫使我们必须使用自定义覆盖物,而自定义覆盖物通常会用 HTML+CSS 来实现。

标注聚合器MarkerClusterer 标注聚合器MarkerClusterer

所以,一个简洁的 Marker 类和复杂的 DOM 结构,会在性能上存在巨大差异,这恰恰是我们加载了 800 多个点就产生性能问题的原因,因为一个“好看”的标注物,居然由 4 个 DOM 节点组成,而这个“好看”的标注物还不知道要怎么样实现 Marker 类里的右键菜单。所以,追求“好看”有问题吗?没有,可华而不实的“好看”,恰恰是性能降低的万恶之源,更不用说,因为覆盖物不会从地图上删除,每次框选都要进行 800 多次的点的检测了,而这些除了开发没有人会在乎,总有人摆出一副“这个需求很简单,怎么实现我不管”的态度……虽然这种方案已经被 Pass 掉了,这里我们还是通过一个简单的示例,来演示下 MarkerClusterer 插件的简单使用吧!以后对于前端类的代码,博主会优先使用 CodePen 进行展示,因为这样子显然比贴代码要生动呀!

See the Pen Marker-Clusterer by qinyuanpei (@qinyuanpei) on CodePen.

这里稍微提带说一下这个插件的优化,经博主测试,在标记物数目达到 100000 的时候,拖拽地图的时候可以明显的感觉的卡顿,这一点大家可以直接在 CodePen 中进行测试。产生性能问题的原因主要在以下代码片段:

   /**
     * 向该聚合添加一个标记。
     * @param {Marker} marker 要添加的标记。
     * @return 无返回值。
     */
    Cluster.prototype.addMarker = function(marker){
        if(this.isMarkerInCluster(marker)){
            return false;
        }//也可用marker.isInCluster判断,外面判断OK,这里基本不会命中
    
        if (!this._center){
            this._center = marker.getPosition();
            this.updateGridBounds();//
        } else {
            if(this._isAverageCenter){
                var l = this._markers.length + 1;
                var lat = (this._center.lat * (l - 1) + marker.getPosition().lat) / l;
                var lng = (this._center.lng * (l - 1) + marker.getPosition().lng) / l;
                this._center = new BMap.Point(lng, lat);
                this.updateGridBounds();
            }//计算新的Center
        }
    
        marker.isInCluster = true;
        this._markers.push(marker);
    
        var len = this._markers.length;
        if(len < this._minClusterSize ){     
            this._map.addOverlay(marker);
            //this.updateClusterMarker();
            return true;
        } else if (len === this._minClusterSize) {
            for (var i = 0; i < len; i++) {
                this._markers[i].getMap() && this._map.removeOverlay(this._markers[i]);
            }
			
        } 
        this._map.addOverlay(this._clusterMarker);
        this._isReal = true;
        this.updateClusterMarker();
        return true;
    };

这段代码主要的问题在于频繁地向地图添加覆盖物,换言之,在这里产生了对 DOM 的频繁修改,具体可参考 _addToClosestCluster 方法。一种比较好的优化是,等所有计算结束后再一次性应用到 DOM。所以,这里我们可以封装一个 render() 方法:

Cluster.prototype.render = function(){
    var len = this._markers.length; 
    if (len < this._minClusterSize) {
            for (var i = 0; i < len; i++) {
                this._map.addOverlay(this._markers[i]);
            }
    } else {
            this._map.addOverlay(this._clusterMarker);
            this._isReal = true;
            this.updateClusterMarker();
    }
}

关于原理介绍及性能对比方面的内容,大家可以参考这篇文章:百度地图点聚合 MarkerClusterer 性能优化

Canvas API 方案

OK,接下来介绍第二种方案,其实从 Canvas API 你就可以想到我要说什么了。Canvas API 是 HTML5 中提供的图形绘制接口,类似于我们曾经接触过的 GDI/GDI+、Direct2D、OpenGL 等等。有没有觉得和游戏越来越近啦,哈哈!百度地图 API v3 中提供了基于 Canvas API 的接口,我们可以把这些“好看”的覆盖物绘制到一个层上面去,显然这种方式会比 DOM 更高效,因为博主亲自做了实验,一次性绘制 10 万个点放到地图上,真的是一点都不卡诶,要说缺点的话嘛,嗯,你想嘛,这都是不是 DOM 了,产品经理那些吊炸天的脑洞还怎么搞?比如最基本的点击,可能要用简单的 2D 碰撞来处理啦,然后就是常规的坐标系转换,听起来更像是在做游戏了,对不对?谁让那么多的游戏都是用 HTML5 开发的呢?同样的,这里给出一个简单的示例:

See the Pen Marker-Clusterer by qinyuanpei (@qinyuanpei) on CodePen.

这个方案真正尝试去做的时候,发现最难的地方是给 Canvas 里的元素绑定事件,细心的朋友会发现,博主在这里尝试了两种方案。**第一种,通过判断点是否在矩形内来判断是否完成了点击,主要的问题是随着点的数目的增加判断的量级会越来越大。第二种,通过 addHitRegion()增加一个可点击区域,这种的性能明显要比第一种好,唯一的限制在于浏览器的兼容性。**目前,需要在 Chrome 中开启 Experimental Web Platform features 。这个探索的过程是相当不易的,大家可以通过 CodePen 进一步感受一下哈!

视野内可见方案

相信大家听完前两个方案都相当失望吧,一个方案用不了,一个方案太麻烦,那这个肯定就是最终可行的方案了吧!猜对了,这真的是体现了大道至简,一开始试着从内存里持久化的数据入手,可最终收到效果的反而是这个最不起眼的方案。简单来说,就是把视野内的覆盖物设为 visible,而把视野外的覆盖物设置 hidden。相当朴素的一种思维对吧,百度地图 API 中有一个返回当前视野的接口 GetBounds(),它回返回一个矩形。所以,我们只需要调用百度接口判断覆盖物在不在这个矩形里就可以了,显然,这里又会循环 800 多次,不过产品经理们都不在乎对吧……顺着这个思路,我们可以写出下面的代码,并在拖动地图和缩放地图的时候调用它:

//监听地图缩放/拖拽事件
map.addEventListener("moveend", showOverlaysByView);
map.addEventListener("zoomend", showOverlaysByView);

//根据视野来显示或隐藏覆盖物
function showOverlaysByView() {
    var bounds = map.getBounds();
    for (var i = 0; i < overlays.length; i++) {
        var overlay = overlays[i];
        var point = overlay._point;
        if (BMapLib.GeoUtils.isPointInRect(point, bounds) || BMapLib.GeoUtils.isPointOnRect(point, bounds)) {
            overlay.show();
        } else {
            overlay.hide();
        }
    }
 }

现在,我只能说,效果挺显著,拖动地图的时候不会卡顿了,因为 visible 和 hidden 的切换会引发浏览器重绘,对于这一切我个人表示满意。当然,这一切离好还很遥远,因为,人类的需要是永无止境的啊。

本文小结

就在我写下这篇博客的时候,产品经理热情洋溢地给我描述了城市配载 2.0 的设想。看了看同类产品的相关设计,我预感这个功能会变成一个以地图为核心的可视化运输系统,这符合国内用户一贯的“大而全”的使用习惯,地图上的交互会更加复杂,需要展示的信息会越来越多,所以,这篇文章里提到的优化,在未来到底有没有用犹未可知。我只能告诉你这样几个原则:尽可能的使用 Marker 类;尽可能的简化 DOM 结构;地图层级变化越大越要考虑使用聚合;视野外的覆盖物该隐藏就隐藏(反正看不到咯……)。一次性加载百万级别数据要求,我从来不觉得合理,因为就算我能加载出来,你能看的过来吗?本身就是伪需求好吧(逃……好了,这就是这篇博客的全部内容啦,以上……