做了几年地图相关的东西,一直在和各种形式的瓦片打交道。前阵子做技术方案评审,又被问到“矢量瓦片和栅格瓦片到底怎么选”,这个老生常谈的问题其实每次都能聊出新东西。尤其是不做GIS专项的人,看到项目资料里同时出现“矢量(vector)瓦片”和“栅格(raster)瓦片”,容易直接把两者理解成“一种旧一种新”,然后闭眼选新的。实际踩过项目坑之后你会发现,这事没那么简单,选错了轻则开发效率低,重则线上问题一波接一波。
这篇就把矢量瓦片和栅格瓦片的底层差异、生产流程、选型思路、以及我在实际项目里踩过的坑系统梳理一遍。不绕弯子,直接上干货,准备上车。
1. 先搞清楚两件事:瓦片是什么,vector和raster争的是什么
1.1 瓦片切图的底层逻辑
不管矢量还是栅格,先说“瓦片”这个概念。地图服务在Web端或者移动端展示时,很少直接把一张巨大无比的地图图片一次性传给浏览器,而是把地图按固定尺寸切成无数个小方块,最常见的是256x256像素或者512x512像素,然后按层级编号,放在类似http://example.com/tiles/{z}/{x}/{y}.png的路径下。前端地图引擎根据当前缩放级别和视野范围,只动态加载视野内需要的那些小图片。
这套金字塔切片模型是几乎所有在线地图的基础。每一级缩放 z 对应的瓦片数量是4^z,从z0只有1张瓦片,z1 四张,z2 十六张,一路指数增长。到了z18级别,理论瓦片数量是个天文数字,好在我们很少需要把全球每一级都切成完整全集,一般只对陆地区域切图,或者只对业务关心的范围切图。
瓦片之所以必须存在,是因为把地图几何数据实时传输给客户端画,在网页初期是不可行的——那时浏览器性能有限,矢量渲染也没成熟引擎,所以服务器先把地图画成图片,客户端只需要展示即可。这就是栅格瓦片的由来。
1.2 别把这里的vector想成C++容器或汽车电子工具链
这里忍不住多说一句。搜索“vector”相关词条时,会出现大量C++的vector容器、二维数组、函数,甚至Vector公司做汽车电子工具链(比如AUTOSAR BSWM下电、DaVinci、CANoe这些)。我做GIS方向的内容时经常看到评论区有人在讨论汽车总线网络相关的Vector工具,或者编程里的vector,和本文的“矢量瓦片”完全是两个世界的概念。
本文说的矢量瓦片里的“矢量”,指的是地图数据以点、线、面这类几何对象的形式存储,而不是预先画好的像素图片。它就像把一份矢量图纸的坐标点传给你,你自己在本地把它画出来。栅格瓦片则像是把设计师已经打印好的照片一张张传给你,你直接摆在页面上就行。
两者本质上的分水岭,就是“地图渲染这活儿到底在服务器干还是在客户端干”。这个差异衍生出了后面所有优缺点,接下来一层层说。
2. 一场渲染与数据的博弈:核心差异逐项拆解
2.1 渲染机制:地图到底是谁画的
栅格瓦片的渲染完全发生在服务器端。你在服务器上装好地图渲染引擎,比如Mapnik、GeoServer、ArcGIS,读取数据库里的空间数据,设置好样式、字体、标注规则,然后切片工具按金字塔规则把地图渲染成PNG或JPEG图片,存下来。客户端加载过程非常简单,就是new Image()然后往canvas或者DOM里放,不需要任何复杂的绘制逻辑。这种机制带来的好处是“所见即所得”,服务器渲染出的效果什么样,用户看到的就是什么样,不同浏览器、不同设备之间几乎不会出现渲染差异。
矢量瓦片的渲染则搬到了客户端。服务端保存的是经过程度组织过的几何数据,前端地图库(Mapbox GL JS、MapLibre GL JS、OpenLayers、Leaflet配插件等)拿到数据后,再根据用户当前设置的style,在浏览器里用WebGL或者Canvas把它画出来。这就好比线下去装修,拿到的是一张设计图纸,自己按需刷成喜欢的墙色、换家具布局。Mapbox GL JS和MapLibre GL这套模型尤其典型,样式和图源完全分离,样式用一套JSON描述,数据源换成矢量瓦片,画出来的最终效果可以完全由前端控制。
这个差异直接带出一个连锁反应:栅格瓦片对客户端性能的要求极低,哪怕是一台很老的手机,只有能显示图片就能流畅拖动地图;矢量瓦片则把一部分计算压力转移到了客户端,高性能设备的画面可以非常丝滑,但低端机在加载复杂图层时,掉帧、卡顿甚至白屏的问题都会被放大。
2.2 体积与加载:同样一块区域,谁更省流量
我刚开始做瓦片优化的时候,一度以为矢量瓦片能“装下”所有地图信息,体积肯定更小,结果被数据狠狠教育了。两种瓦片的体积特点不能简单用一个“小”字概括,得分开看。
先看栅格瓦片。切好的PNG、JPEG是有规律可循的,城镇密集区域细节多,瓦片体积可能冲到100KB以上,乡村或海洋区域纯色多,PNG压缩后可能只有几KB。但问题是栅格瓦片是一层一层独立切的,z15、z16、z17每层都要存一套完整图片。如果你要支持用户从卫星视图缩放到街道级别,那得把每一层全部切出来。同一块区域,栅格瓦片在多个层级上会重复存储道路、建筑、标签等信息,这造成非常可观的存储膨胀。我参与过的某市级项目,切一套全地域的18级高细节栅格瓦片,存储量轻松突破几百GB,真实生产里如果还包含卫星影像,那就得按TB算了。
再来看矢量瓦片。矢量瓦片存放的是几何坐标和属性,同一套数据可以支撑多个缩放级别的显示,不需要每个z都单独存一份等量细节。用墨卡托投影的MVT切片,数据经过压缩,几何也在切片时做了简化,典型的道路网、建筑轮廓数据,单块瓦片的大小常常只有十几KB到几十KB。同样的市级区域,全量矢量瓦片压缩后可能只有几GB到几十GB。虽然不是什么魔法级缩小,但相比栅格,在存储和带宽上的优势非常明显。
不过,矢量瓦片也有自己的“反直觉坑”。当数据源几何非常复杂时,比如超细精度的海岸线、密集等高线、高精度的面状植被边界,切出来的MVT并不一定小。如果前端再同时加载好几层矢量源(路网、POI、建筑、水系、地形),并发请求量和网络消耗也可能超过一个简单的栅格影像方案。真要说“谁更省流量”,得结合数据复杂度和应用场景判断。
2.3 样式与交互:改个配色要不要重新切图
栅格瓦片最大的痛点之一,就是“改样式 = 重新切图”。曾经有客户提需求,觉得地图上的主干道颜色不够明显,想从黄色改成橙色。听起来是个三分钟能解决的调整,但在栅格瓦片体系里,你得修改样式表、重新渲染切片、重新发布、再清掉CDN和客户端缓存,整个过程少则几小时多则一两天。要是地图上有几十种图斑风格、多种语言标签切换,那每次调整都是一场小规模上线战役。
矢量瓦片在这方面几乎是无敌的。样式和数据分离,前端style JSON里面改一个颜色属性,页面刷新立刻就能看到新效果;想切换深色模式、夜间模式、高对比色,在上层实现个样式切换就行,完全不用动瓦片数据。对于有大量个性化地图需求的场景,比如车内导航白天/黑夜模式切换、智慧大屏换肤、不同业务线想要不同品牌色地图,矢量瓦片这种“数据不变只换妆”的模式实在太省事了。
交互上,矢量瓦片同样占优势。前端拿到的是几何数据,可以轻松实现要素的点击查询、hover高亮、动态过滤(比如只显示4S店、不显示加油站)、要素级别的动画和缩放过渡。栅格瓦片虽然也能在瓦片之上叠一层业务数据去做点击事件,但一切基于离散图片,想动态控制底图样式,基本办不到。这次选型时如果产品经理明确要求“地图要支持卫星、街道、深色三种模式切换”,那栅格方案基本可以直接排除了,会累死运维和前端。
2.4 更新维护:数据变了,谁跟进得更快
地图数据永远在变。新增一条路、楼盘封顶了要更新轮廓、河流改了道、行政区划调整了边界。这时候更新方式也把两种瓦片的差异暴露得清清楚楚。
栅格瓦片的数据更新是“重切”逻辑。哪怕只改了某个区域的一小段路,最稳妥的做法是把该范围内受影响的所有z层瓦片全部重切,然后再更新到缓存服务里。如果原图源是基础底图级别的大范围数据,每更新一次就等于做一次全量发布,属于投入工时很高、频率必须压低的操作。这也是为什么有些底图平台一个月甚至一季度才更新一次底图样式和数据,不是懒,是真的切图成本太高。
矢量瓦片更适合增量更新。当源数据库里的数据发生变化后,可以只重新生成受影响的某些瓦片,或者做局部范围的再切片打包。因为客户端最终是按需加载的,只要更新后的瓦片缓存覆盖了受影响的区域,用户下一次访问就能看到最新数据。如果用的是PostGIS配合ST_AsMVT动态生成瓦片,甚至可以做“实时切片”,数据库数据一变,瓦片接口吐出来的结果就自动包含新要素,完全不需要人工切图发布。这种实时性对于路况图层、实时OA工单分布、车辆轨迹这类高频变化数据来说,几乎是刚需。
3. 从零做一套瓦片的实操路径
讲完理论,上实操。我会用最常见的技术栈做一套最小可用方案,覆盖矢量瓦片和栅格瓦片两条链路,工具选了开源生态为主,方便大家在本地复现。
3.1 基于OSM和PostGIS做矢量瓦片
做矢量瓦片最经典的数据来源是OpenStreetMap(OSM)数据。先用osm2pgsql把.osm.pbf文件导入PostgreSQL + PostGIS,之后可以用数据库函数直接生成MVT(Mapbox Vector Tile)。这套流程对本地测试和中小规模项目非常友好。
第一步是准备数据库与扩展。Debian/Ubuntu系统上安装postgresql、postgis,然后执行:
CREATE EXTENSION IF NOT EXISTS postgis;导入OSM数据。以某城市区域的pbf文件为例:
osm2pgsql -c -d gis --slim -C 8000 -H localhost -U postgres city.osm.pbf其中-C 8000表示给osm2pgsql分配8GB内存做缓存,机器内存小的话可以降到2048或更低,--slim模式会使用数据库中间表,避免内存吃满。
表建好之后,用PostGIS的ST_AsMVT函数动态生成矢量瓦片。比如从planet_osm_line表抽取出道路数据生成瓦片:
SELECT ST_AsMVT(tile, 'roads', 4096, 'geom') FROM ( SELECT osm_id, highway, name, ST_AsMVTGeom( ST_Transform(way, 3857), ST_TileEnvelope(${z}, ${x}, ${y}), 4096, 256, true ) AS geom FROM planet_osm_line WHERE way && ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 4326) AND highway IS NOT NULL ) AS tile;这里比较关键的是ST_AsMVTGeom这一步。它把数据库里的真实坐标转换到MVT瓦片内部的局部坐标空间(默认4096x4096),同时做了必要的裁剪和取整。直接用原始坐标返回是不行的,前端地图库拿到的不能是地理坐标,必须是瓦片内部坐标。参数里的ST_TileEnvelope根据z/x/y算出当前瓦片对应的Web墨卡托范围,ST_Transform(..., 3857)是因为MVT要求数据输出在EPSG:3857坐标系下,而原始way一般是EPSG:3857,不等的时候必须转换。
做完这一步,一个接口就能动态吐MVT二进制流。前端用MapLibre GL或Mapbox GL访问这个瓦片源,设置一个最小style即可显示。实际经验里,这种“数据库实时切片”的方案适合数据量不太大(比如一个城市区域单层道路要素不超过几百万行)的场景,数据量和并发一高,数据库压力会很大,届时就得考虑Tippecanoe这种离线批量切片方案。
3.2 用Tippecanoe快速把GeoJSON转MVT
Tippecanoe是Mapbox开源的一个切片工具,非常适合把GeoJSON、GeoJSONSeq这类矢量数据批量转成MVT。它的特点是速度极快、参数灵活,还能做要素简化、抽稀、按层级分级显示。
一条最常用的命令大概长这样:
tippecanoe -e output_tiles -Z 0 -z 14 -f -l building -o buildings.mbtiles building.geojson-Z 0 -z 14表示从缩放级别0切到14级;-f是强制覆盖已有输出;-l building设置图层名;-o输出mbtiles文件,这是SQLite格式的瓦片包,也可以用-e直接输出目录瓦片。
如果你希望不同缩放级别显示不同精度的数据,用-r设置简化级别,或让Tippecanoe自动根据z层级对要素做抽取,也是这套工具的核心玩法。比如:
tippecanoe -e output_tiles -Z 0 -z 16 -r 2.5 -o output.mbtiles big.geojson这里的-r 2.5表示在切片时允许对几何做平均2.5倍率的简化。数值越大,高缩放级别下几何越粗糙,瓦片体积越小,载入越快。实际项目里可以先粗切一版看看视觉效果,再回调整体参数,找到质量和体积的平衡点。
Tippecanoe生成mbtiles后,可以直接通过mb-util解包成目录瓦片,或者用nginx配合瓦片服务接口直接读mbtiles。生产实践里常见做法是把mbtiles部署到S3或OSS,再用CDN分发,前端地图引擎直接请求https://cdn.example.com/tiles/roads/{z}/{x}/{y}.pbf即可。
3.3 栅格瓦片的经典生产链路(GeoServer + MapProxy/nginx)
虽然矢量瓦片现在很火,但栅格瓦片在遥感影像、卫图、老版本底图项目里依然大量存在。很多项目不是“二选一”,而是底图分析用栅格影像、业务图层用矢量瓦片。
栅格瓦片最常见的生产链路是GeoServer发布WMS/WMTS格式的切片,或者用Mapnik渲染,再用MapProxy做切片缓存。
比如在GeoServer里配置好数据源和样式后,直接请求WMS服务:
http://localhost:8080/geoserver/wms?service=WMS&version=1.1.0&request=GetMap&layers=workspace:roads&bbox={bbox}&width=256&height=256&srs=EPSG:3857然后把WMS地址挂到MapProxy里,配置一个缓存源,按金字塔规则预生成PNG切片:
sources: wms_source: type: wms req: url: http://localhost:8080/geoserver/wms layers: workspace:roads caches: my_cache: grids: webmercator sources: [wms_source] format: image/png layers: - name: my_raster_layer title: Raster Tiles Demo sources: [my_cache]之后用mapproxy的seed命令批量预热切片:
mapproxy-util create -t base -o mapproxy.yaml mapproxy-util serve-develop mapproxy.yaml mapproxy-util seed -f mapproxy.yaml -f mapproxy_seed.yaml如果不想引入GeoServer这种重型服务,直接用Mapnik写XML样式表渲染PNG块也是一种主流做法,只是配置复杂度更高,对开发者的地图美化能力要求也更高。生产环节里注意一下,栅格瓦片切出来后一定要做好nginx/CDN的缓存标头,不然后端会在高并发下被打爆。
4. 项目实战中踩过的坑和排查手记
4.1 矢量瓦片的低端机性能漩涡
某次项目里,我们信心满满地把整个城市路网、POI、建筑层全部切成了MVT,前端用Mapbox GL渲染。在开发用的M系列芯片MacBook上一切丝滑,帧率稳定60FPS,结果拿到测试的老款安卓手机上,缩放到城市级别时地图直接卡成PPT,滑动一下要等两三秒才跟上,甚至出现过WebView崩溃。
排查后发现原因有几层:一是数据切得“太满”,瓦片里要素数量超多且每个要素层级都保留了极高精度的坐标点,前端为了绘制这些几何要处理大量顶点数据;二是样式里开着全局阴影和多种标签碰撞算法,重绘压力巨大;三是viewPort设备分辨率高,WebGL在低端GPU上渲染大规模图层,性能直接被拖垮。
解决办法是给矢量数据做“分级抽稀”。低缩放级别只返回主干道和少量POI,高缩放级别才返回建筑等细节。Tippecanoe切图时用-r、-B和金字塔分级参数,数据库实时切片时用ST_SimplifyPreserveTopology配合z层级简化几何,前端样式里也关闭不必要的特效。经过这轮优化,同样的数据量,瓦片体积压缩了约50%,低端机上的渲染帧率明显回升,基本稳定在25-30FPS以上。这个坑让我意识到,矢量瓦片不是“切完就完”,性能需要从数据到样式到设备三端协同优化。
4.2 栅格瓦片的清晰度与缓存标头问题
栅格瓦片最常被吐槽的就是清晰度问题。在高分屏(比如笔记本2K屏或手机Retina屏)上显示时,如果按传统方式加载256像素普通瓦片,缩放后边缘文字会虚,画面有颗粒感。曾经被设计师盯着改了好几轮,后来发现根因是高清屏下设备像素比(DPR)大于1,地图引擎一个CSS像素实际对应1.5个甚至2个物理像素,切图却还是按1x画的。
解决方案有两种:第一种是在高清屏场景下加载更高分辨率的瓦片,把请求参数里的瓦片大小从256改成512,但代价是瓦片存储体积膨胀4倍左右;第二种是在前端用CSS transform对大图做缩放。第一种是主流做法,能保证视觉清晰,但需要提前评估存储和带宽成本。如果项目里主要是大屏展示、对清晰度要求很高,建议直接把瓦片切成512x512,配合按照DPR动态切换URL。
另外还有一个很容易忽略的坑:栅格瓦片缓存的HTTP标头。如果不设置Cache-Control: max-age或ETag,前端和CDN每次都会回源到切片服务取瓦片,并发量一高,后端CPU直接拉满。后来我们在nginx里给瓦片路径设置:
location /tiles/ { expires 30d; add_header Cache-Control "public, immutable"; }这种做法在瓦片内容不变的情况下非常有效,缓存命中率可以提升到95%以上,有效缓解瓦片服务的压力。
4.3 投影和切片范围的隐性坑
不管是矢量还是栅格,Web地图绕不开投影问题。当前绝大部分Web瓦片采用Web墨卡托投影(EPSG:3857),瓦片分块规则也是按这个投影定义的。如果你后端用的数据坐标系是WGS84(EPSG:4326,经纬度),切图前不转换,会导致地图显示错位、要素偏离到奇怪的位置。
曾经帮一个朋友排查问题,他直接用PostGIS的ST_AsMVT生成瓦片,前端地图偏移得厉害,所有道路都跑到海里去了。检查后发现他从planet_osm_line表取数时,way字段是3857坐标,但他忘了用ST_Transform(way, 3857),而是在SQL里直接把3857当4326用,差不多把地图整体挪了一个维度。这个教训提醒我,所有瓦片相关的坐标系转换都要明确写清楚“输入什么,输出什么”,不要依赖数据库字段的默认坐标。
还有切片范围的问题。MVT规范要求瓦片内部坐标被限制在0到4096的范围内,如果几何跨越瓦片边界,必须分别裁剪到相邻瓦片,同时保留一定“缓冲”区域以避免要素被截断产生缝隙。有时候要素在相邻瓦片边界连接处出现细线断口,多半就是因为裁剪缓冲区大小设置不够。ST_AsMVTGeom里的buffer参数(上面示例的256),就是控制这个宽度。这个参数不能随意设成0,不然复杂要素拼接会非常难看。
5. 选型思路与落地建议
5.1 什么场景闭眼选栅格
栅格瓦片并没有过时,在很多场景下反而更省心。比如卫星影像、航拍影像这类本身就是图片的数据,一定是栅格,做成矢量得不偿失。再比如对视觉效果要求极高、需要完全一致渲染结果的大屏可视化项目——服务器渲染好的图片,在不同设备上观感都一样,矢量瓦片还得担心浏览器差异和字体缺失问题。另外,如果前端团队对地图引擎不熟悉,不想引入复杂的地图类库,或者项目只需要一张基础底图,不需要做交互点击和样式切换,那栅格瓦片从开发效率上来说仍然是首选。
这里也建议业务里把栅格和矢量做混合使用。例如卫星影像用栅格切片,其上叠加道路、行政区划、业务点用矢量瓦片。两者不冲突,反而能发挥各自优势。
5.2 什么场景果断上矢量
反过来,需求里存在以下特征时,矢量瓦片基本就是最优选了:底图视觉会经常调整(品牌色、夜间模式);需要做要素级交互(点击查询、高亮、圈选、动态过滤);数据更新频繁且希望低时延地反映到前端;在意流量成本和瓦片存储规模。尤其是车载导航、LBS业务后台、城市大脑这种要频繁换肤、做大量数据展示和交互的系统,矢量瓦片能救你于水火。
还有一点很实用:矢量瓦片天然支持属性信息嵌入,可以做到“同一个图层数据,在不同视图下展示不同字段”。栅格瓦片如果想做这种多业务字段的动态展示,得来回切图层,工程复杂度和渲染压力都会大一个量级。
5.3 我的个人经验和延展建议
做技术选型时我总结出一条大概经验:先看产品经理对“地图能支持哪些交互”的回答,再问“地图视觉多久会变一次”,最后再算一下手上有多少机器和存储。前两个问题如果答案是“要很多交互、经常换肤”,那别纠结,直接上矢量;如果答案是“就是展示一张图,三年不改”,栅格省事到极致。存储和带宽是最后才考虑的问题,因为矢量瓦片虽说多数时候更小,但它在客户端的开发、测试成本通常比栅格高,这部分隐性投入不少项目低估了。
另外,如果团队之前完全没有地图渲染的技术积累,建议先拿MapLibre GL这类成熟开源库搭一个最小demo,把Tippecanoe切一份全国road数据试一下,一天之内就能直观感受到矢量瓦片的利与弊。等摸清性能边界后,再去接PostGIS动态切片或在业务系统里用Vector风格做深度定制,会顺畅得多。
最后分享一个我后期会常花时间做的小事情:给瓦片服务做好可观测性。不管栅格还是矢量,都得记录每个z/x/y的请求量、耗时、缓存命中率和错误码。真出问题的时候,比如某一块区域瓦片大面积加载失败,或者CDN回源异常,有这些日志辅助定位的效率能提升一个数量级。地图瓦片是典型的“平时不觉得重要、崩了才知道要命”的环节,尽早在上线前把监控和降级方案做实,才是保证线上地图稳定性的关键一步。