引言
标注是地图最基本的元素之一,标明了地图每个位置或线路的名称。在地图 JSAPI 中,标注的展示效果及性能也是需要重点解决的问题。
新版地图标注的设计中,引入了 SDF ( signed distance field)重构了整个标注部分的代码。新的方式需要把标注的位置偏移,避让,三角拆分等全部由前端进行计算,不仅计算量激增,内存的消耗也成了重点关注的问题之一。
例如,3D 场景下需要构建大量的顶点坐标,一万左右的带文字的标注,数据量大约会达到 8 (attributes) 5 (1个图标 + 4个字) 6(个顶点) 1E4 ,约为 250w 个顶点,使用 Float32Array 存储,需要的空间约为 2.5E6 4(byte)空间(海量地图标注 DEMO)。前端这样大量的存储消耗,需要对内存的使用十分小心谨慎。于是借此机会研究了一下前端内存相关的问题,以便在开发过程中做出更优的选择,减少内存消耗,提高程序性能。
01 前端内存使用概述
首先我们来了解一下内存的结构。
内存结构
内存分为堆(heap)和栈(stack),堆内存存储复杂的数据类型,栈内存则存储简单数据类型,方便快速写入和读取数据。在访问数据时,先从栈内寻找相应数据的存储地址,再根据获得的地址,找到堆内该变量真正存储的内容读取出来。
在前端中,被存储在栈内的数据包括小数值型,string ,boolean 和复杂类型的地址索引。
所谓小数值数据(small number), 即长度短于 32 位存储空间的 number 型数据。
一些复杂的数据类型,诸如 Array,Object 等,是被存在堆中的。如果我们要获取一个已存储的对象 A,会先从栈中找到这个变量存储的地址,再根据该地址找到堆中相应的数据。如图:
简单的数据类型由于存储在栈中,读取写入速度相对复杂类型(存在堆中)会更快些。下面的 Demo 对比了存在堆中和栈中的写入性能:
function inStack(){
let number = 1E5;
var a;
while(number--){
a = 1;
}
}
var obj = {};
function inHeap(){
let number = 1E5;
while(number--){
obj.key = 1;
}
}
实验环境1:
mac OS/firefox v66.0.2
对比结果:
实验环境2:
mac OS/safari v11.1(13605.1.33.1.2)
对比结果:
在每个函数运行 10w 次的数据量下,可以看出在栈中的写入操作是快于堆的。
对象及数组的存储
在JS中,一个对象可以任意添加和移除属性,似乎没有限制(实际上需要不能大于 2^32 个属性)。而JS中的数组,不仅是变长的,可以随意添加删除数组元素,每个元素的数据类型也可以完全不一样,更不一般的是,这个数组还可以像普通的对象一样,在上面挂载任意属性,这都是为什么呢?
Object 存储
首先了解一下,JS是如何存储一个对象的。
JS在设计复杂类型存储的时候面临的最直观的问题就是,选择一种数据结构,需要在读取,插入和删除三个方面都有较高的性能。
数组形式的结构,读取和顺序写入的速度最快,但插入和删除的效率都非常低下;
链表结构,移除和插入的效率非常高,但是读取效率过低,也不可取;
复杂一些的树结构等等,虽然不同的树结构有不同的优点,但都绕不