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

推荐订阅源

cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
The Blog of Author Tim Ferriss
S
Securelist
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Tor Project blog
S
Schneier on Security
Google DeepMind News
Google DeepMind News
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Blog — PlanetScale
Blog — PlanetScale
AWS News Blog
AWS News Blog
F
Fortinet All Blogs
C
Cisco Blogs
L
LangChain Blog
MongoDB | Blog
MongoDB | Blog
D
Docker
T
The Exploit Database - CXSecurity.com
The GitHub Blog
The GitHub Blog
月光博客
月光博客
量子位
A
Arctic Wolf
博客园 - 叶小钗
L
LINUX DO - 热门话题
J
Java Code Geeks
The Hacker News
The Hacker News
Security Latest
Security Latest
V
Vulnerabilities – Threatpost
M
MIT News - Artificial intelligence
博客园 - 【当耐特】
T
Tenable Blog
Microsoft Security Blog
Microsoft Security Blog
Recent Announcements
Recent Announcements
Simon Willison's Weblog
Simon Willison's Weblog
B
Blog
阮一峰的网络日志
阮一峰的网络日志
A
About on SuperTechFans
博客园_首页
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Threat Research - Cisco Blogs
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Y
Y Combinator Blog
PCI Perspectives
PCI Perspectives
Hacker News: Ask HN
Hacker News: Ask HN
P
Palo Alto Networks Blog
小众软件
小众软件
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
O
OpenAI News
P
Privacy International News Feed
P
Privacy & Cybersecurity Law Blog
N
Netflix TechBlog - Medium

元视角

.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 图像风格化迁移助力画家梦想 - 元视角 利用 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 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 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
百度地图加载海量标注性能优化策略 - 元视角
飞鸿踏雪 · 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 结构;地图层级变化越大越要考虑使用聚合;视野外的覆盖物该隐藏就隐藏(反正看不到咯……)。一次性加载百万级别数据要求,我从来不觉得合理,因为就算我能加载出来,你能看的过来吗?本身就是伪需求好吧(逃……好了,这就是这篇博客的全部内容啦,以上……