问题描述
http://www.mypxh.com/
雪花网站中的故事是通过地图检测中心点的变化,并按照指定范围的半径,发送数据请求得到的。在下列场景中会出现大量不必要的请求:
- 用户频繁拖动地图的时候,数据变化不大
- 用户查看大半径的数据,如全国范围,此时大部分数据已经拿到
优化意义
- 减轻服务器压力
- CDN按次收费
解决方案
我们要做的就是标记地图上已搜索的点,发送请求前先检查,如果发现要搜索的点未被标记,则发送数据请求,收到返回数据后更新标记范围。
数学建模
首先,以地图中心点为中心,2r为边长的正方形表示的是一片连续的范围,想要标记这个范围,最简单的方法是采样法,化连续为离散。拿到的中心点是六位小数,根据实际情况, 采用两位小数就可以达到较好的体验。
确定了采样点,接下来就是处理数据了。数组下标只能是整数,所以我们给每个经纬度乘以100,化小数为整数。纬度的范围是[-90,90],经度的范围是[-180,180],要想标记地图上所有点,就需要一个mn([902100, 1802*100])的二维数组,这个数据量非常大。初始化这样大的数组需要耗费大量内存。
位运算节省内存
手机上的内存非常宝贵,我们可以采取时间换空间的策略。
然后用位运算压缩数据大小。
js中Number类型的实质是一个64位的浮点数,11位表示指数,1位表示正负,我们取32位来表示经度纬度位数48的点阵列,可以把数据压缩成[902100/8, 1802*100/4]的大小。
那么我们怎么存取某一位数据呢?用掩码(MASK)数组,MASK[i] = Math.pow(2, i)。取数据的时候和掩码数组进行逻辑与运算,存数据的时候逻辑非运算。
初始化地图数组时,依旧比较大,但其实没有初始化过的点是null,与初始化的0一样是逻辑非,不影响取值,因此我们可以只初始化一个长度为902100/8,内容为空的数组。这样,搜索过的点为1,未搜索过的点为null。此时我们已经把初始化的任务缩小了1802100 *8 = 288000倍。
web worker 初始化数组
js是单线程的。初始化数组比较耗时,会阻塞其他任务。一开始初始化会阻塞页面渲染,造成londing时间过长;放在地图渲染后会影响用户的操作,给用户拖不动的感觉。此时我们采用webWorker方案,另起一个线程。
Web-Worker.png
Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面,很适合处理计算密集型的工具。worker通过new worker(worker.js) 来新建worker进程,通过postMessage和onMessage来与主进程通信。worker.js必须是外部文件,这个可以通过webpakc的CopyWebpackPlugin插件来指定外部文件路径,一般情况是在'../static'路径下。
主进程中,postMessage和onMessage 机制太过简单,不利于事件的管理。所以要进一步封装workerManager 类。事件的类型用action表示,检查是否已搜索和更新已搜索点,分别命名为check和fresh。在发起事件的时候(postMessage)给每个事件添加一个带有ID,action和payload的监听者(listener)用与处理回调(onMessage)。
worker收到事件,根据action选择相应的处理函数,并根据payload处理数据并返回结果给主进程。主进程收到结果,取消此id的listener,进行下一步处理。
成果
我们用三天的时间形成了一个比较完善的地图数据缓存方案。这种性能优化在平时的业务中不常见,是一次比较有意义的探索,不但积累了工程化解决具体问题的经验,也形成了可复用的work和位运算的代码。
其实,探索方案的过程还是比较曲折的,比如
- r的单位是公里,而中心点的单位是经纬度,忘记转换单位;
- 使用数组前,尝试用hash表来标记已搜索的经纬度和r,但是字符串的处理速度比较慢,效果很差。
- 使用worker之前,尝试了setTimeout的方案,即把一次性运行完的初始化任务分解成多次,每次初始化一部分,给其他任务执行的机会,来缓解阻塞的现象,但实际的总时间并没有太大的改善,所以最终还是选择了worker方案。
最终网站的效果的提升是非常可观的,这也带给我极大的成就感。继续加油吧!
网友评论