# Best Practices for Hybrid MapLibre/Cesium Architecture: Handling MVT, Terrain, and Complex Styling in Production

**URL:** <https://community.cesium.com/t/best-practices-for-hybrid-maplibre-cesium-architecture-handling-mvt-terrain-and-complex-styling-in-production/46031>\
**Category:** CesiumJS\
**Created:** [April 29, 2026, 3:45am UTC](https://community.cesium.com/t/best-practices-for-hybrid-maplibre-cesium-architecture-handling-mvt-terrain-and-complex-styling-in-production/46031 "2026-04-29T03:45:15Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![zanzandora](https://sea2.discourse-cdn.com/cesium/user_avatar/community.cesium.com/zanzandora/32/28173_2.png) [@zanzandora](https://community.cesium.com/u/zanzandora)\
**Post date:** [April 29, 2026, 3:45am UTC](https://community.cesium.com/t/best-practices-for-hybrid-maplibre-cesium-architecture-handling-mvt-terrain-and-complex-styling-in-production/46031/1 "2026-04-29T03:45:15Z")

</div>

Hi,

I am currently working on a hybrid mapping application (MapLibre GL JS for 2D, CesiumJS for 3D) for a large-scale Villa & Resort project. Our goal is to maintain visual consistency when users switch between the 2D and 3D views.

**Our Data & Setup:**

- We use custom Vector Tiles (MVT) served from our backend.

- Our Mapbox style JSON is quite complex, utilizing `match`, `case`, and `interpolate` expressions for base layers (roads, water, sidewalks, boundaries).

- We use custom Sprite atlases for POIs, trees, and labels.

- In Cesium, we have 3D Terrain enabled.

**The Problem:** Initially, we wanted to reuse the MVT and Mapbox style directly in Cesium. We tried using `cesium-vector-provider` (which uses MapLibre under the hood to render to a canvas and drape it over Cesium as an `ImageryProvider`). However, this approach is not viable for our production environment due to several critical issues:

1. **Performance Bottleneck:** Running MapLibre in the background to rasterize vector tiles causes severe VRAM overhead and frame drops.

2. **Distortion on Terrain:** When draped over 3D terrain, text labels and POI icons get baked into the raster image. They stretch and distort along the slopes instead of behaving like proper Billboards facing the camera.

3. **Texture Stretching:** At low pitch angles, the draped imagery becomes blurry/pixelated unless we generate excessively high-resolution canvases.

**Our Proposed Architecture (Seeking Validation):** To solve this, we are considering completely decoupling the data pipeline for the 3D mode:

1. **Base Map (Roads, Water, Grass):** Move rasterization to the Server-Side. Use a tile server to consume our MVT + Style and serve standard XYZ Raster Tiles (`.png` / `.webp`) to Cesium’s `UrlTemplateImageryProvider`.

2. **3D Buildings:** Convert building footprints to **3D Tiles** (`.b3dm` or `.glb`) via our backend pipeline.

3. **POIs & Labels:** Fetch POI data via a separate JSON/GeoJSON API. Load our sprite atlas (`sprite.png` & `sprite.json`) and render them natively in Cesium using `BillboardCollection` (`imageSubRegion`) and `LabelCollection` so they orient correctly in 3D space.

**My Questions:**

1. Is this decoupled approach (Server-side raster for base + 3D Tiles + Native Billboards) considered the industry standard for this kind of Hybrid MVT/Cesium architecture?

2. Are there any recommended tools or pipelines for server-side rasterization that perfectly replicate Mapbox GL JS styling (especially complex expressions)?

3. Is there any native feature on the Cesium roadmap that will handle Mapbox Vector Tiles and their styling more elegantly in the future?

Any insights, advice, or alternative approaches would be greatly appreciated. Thank you!

---

<div class="post-metadata">

**Author:** ![donmccurdy](https://sea2.discourse-cdn.com/cesium/user_avatar/community.cesium.com/donmccurdy/32/4683_2.png) [@donmccurdy](https://community.cesium.com/u/donmccurdy)\
**Post date:** [April 29, 2026, 2:43pm UTC](https://community.cesium.com/t/best-practices-for-hybrid-maplibre-cesium-architecture-handling-mvt-terrain-and-complex-styling-in-production/46031/2 "2026-04-29T14:43:08Z")

</div>

Hi @zanzandora!

Support for MVT is CesiumJS is indeed on the roadmap, and you can find initial work in progress here:

> <https://github.com/CesiumGS/cesium/pull/13404>
>
> \# Description
> This PR introduces an MVT loading pipeline that decodes MVT tile …payloads, converts them to vector glTF, and renders them through \`VectorGltf3DTileContent\` in the 3D Tiles runtime path.
> 
> It also improves missing-tile handling by treating HTTP 404/204 MVT tile responses as empty content, allowing sparse datasets to render gracefully.
> 
> \<img width="1093" height="803" alt="image" src="https://github.com/user-attachments/assets/f7df7360-5366-45aa-8f7c-a4d506584a06" /\>
> 
> 
> https://github.com/user-attachments/assets/cefa4d6c-aef7-48cd-9ff7-fa1948ca4dd3
> 
> 
> \## Overall Pipeline
> 
> In \`MVTDataProvider.js\`, \`MVTDataProvider.fromUrlTemplate(...)\` creates the provider.
> 
> \*\*\`MVTDataProvider\`\*\* (extends \`UrlTemplate3DTilesDataProvider\`)
> \- Handles configuration (\`urlTemplate\` / \`minZoom\` / \`maxZoom\` / \`extent\` / \`featureIdProperty\`), optionally hydrating defaults from \`metadata.json\`
> \- Builds a runtime-generated 3D Tiles tileset that maps \`{z}/{x}/{y}\` URL templates to tile headers
> \- Implements \`\_createCodec()\` to inject an MVT-aware \*\*runtime content codec\*\* onto \`tileset.\_runtimeContentCodec\`. The codec carries:
> - \`createContent(tileset, tile, resource, arrayBuffer)\` — how to turn raw bytes into a \`Cesium3DTileContent\`
> - \`disableSkipLevelOfDetail: true\` — vector tiles do not support skip-LOD
> - \`missingTilePolicy: { statusCodes: \[404, 204\], urlPattern }\` — sparse datasets render gracefully
> 
> \*\*\`Cesium3DTile\` / \`Multiple3DTileContent\`\*\* (runtime, format-agnostic)
> \- On each tile load, \`makeContent(...)\` checks \`tileset.\_runtimeContentCodec\`
> \- If a codec is present, dispatches directly to \`codec.createContent(...)\` — bypassing magic-byte sniffing in \`preprocess3DTileContent\` and the \`Cesium3DTileContentFactory\` lookup
> \- If a request fails, \`isMissingTileContentError\` consults \`codec.missingTilePolicy\` to convert HTTP 404 / 204 responses into \`Empty3DTileContent\`
> \- Runtime never names "MVT" — adding new vector formats (MLT, GeoJSON tiles, etc.) only requires a new provider with its own codec
> 
> \*\*MVT codec\*\* (the closure returned by \`MVTDataProvider#\_createCodec\`)
> \- Receives raw bytes for a single MVT tile (URL extension is irrelevant)
> \- Calls \`decodeMVT(...)\` to parse tile-local geometry and properties from the protobuf payload (with bounds-checked varints and truncation / over-long defenses)
> \- Parses \`{z}/{x}/{y}\` from the tile URL (suffix optional; supports query / fragment)
> \- Calls \`buildVectorGltfFromDecodedTile(...)\` to convert parsed features into vector glTF JSON using the \`CESIUM\_mesh\_vector\` extension (and optional \`EXT\_mesh\_features\` when \`featureIdProperty\` is set)
> \- Returns \`Empty3DTileContent\` for tiles that decoded to nothing, otherwise calls \`VectorGltf3DTileContent.fromGltf(...)\` to reuse the existing vector rendering / feature path
> 
> \*\*\`VectorGltf3DTileContent\`\*\*
> \- Converts glTF primitives into \`BufferPoint\` / \`BufferPolyline\` / \`BufferPolygon\` collections
> \- Participates in Cesium's picking / styling / update lifecycle
> 
> So:
> 
> MVT bytes → provider-injected codec → decoded vector features → vector glTF → existing Cesium vector tile content path.
> 
> Runtime is decoupled from MVT specifics: the same \`\_runtimeContentCodec\` hook will host MLT and any future tile decoders without changes to \`Cesium3DTile\`, \`Multiple3DTileContent\`, \`preprocess3DTileContent\`, or \`Cesium3DTileContentFactory\`.
> 
> 
> 
> 
> 
> 
> 
> 
> 
> \## Issue number and link
> 
> 
> 
> 
> 
> \## Testing plan
> Test files:
> \[highway\_roads.zip\](https://github.com/user-attachments/files/26802744/highway\_roads.zip)
> \[highway\_roads\_no\_clipping.zip\](https://github.com/user-attachments/files/26802747/highway\_roads\_no\_clipping.zip)
> \[highway\_roads\_uncompressed.zip\](https://github.com/user-attachments/files/26802756/highway\_roads\_uncompressed.zip)
> 
> Sandcastle code:
> \`\`\`
> const viewer = new Cesium.Viewer("cesiumContainer");
> 
> const mvt = await Cesium.MVTDataProvider.fromUrlTemplate(
> "http://localhost:8082/highway\_roads\_uncompressed/{z}/{x}/{y}.pbf",
> {
> minZoom: 0,
> maxZoom: 6,
> extent: Cesium.Rectangle.fromDegrees(
> -159.780010, 19.059930, -66.979990, 70.191920
> ),
> },
> );
> 
> viewer.scene.primitives.add(mvt);
> viewer.camera.flyTo({
> destination: Cesium.Rectangle.fromDegrees(
> -159.780010, 19.059930, -66.979990, 70.191920
> ),
> });
> 
> \`\`\`
> 
> 
> 
> \## Author checklist
> 
> \- \[x\] I have submitted a Contributor License Agreement
> \- \[x\] I have added my name to \`CONTRIBUTORS.md\`
> ~~- \[\] I have updated \`CHANGES.md\` with a short summary of my change~~
> \- \[x\] I have added or updated unit tests to ensure consistent code coverage
> \- \[x\] I have updated the inline documentation, and included code examples where relevant
> \- \[x\] I have performed a self-review of my code
> 
> \## AI acknowledgment
> 
> \- \[x\] I used AI to generate content in this PR
> \- \[x\] If yes, I have reviewed the AI-generated content before submitting
> 
> If yes, I used the following Tools(s) and/or Service(s):
> Github Copilot
> 
> 
> If yes, I used the following Model(s):
> Claude Opus 4.7 for code review, GPT 5.3 codex to debug some decode logic

Additionally, we’re working to allow including vector features directly in 3D Tiles, similar to MVT:

> <https://github.com/CesiumGS/3d-tiles/issues/825>
>
> This is an idea I've been playing around with for a while. Can we use glTF for v…ector data? Specifically, large amounts of 3D cartesian vector data. Here are some initial ideas.
> 
> \### Points
> 
> \* Use primitive \`mode: 0\` (\`POINTS\`)
> \* Use \[\`EXT\_mesh\_features\`\](https://github.com/CesiumGS/glTF/blob/proposal-EXT\_mesh\_features/extensions/2.0/Vendor/EXT\_mesh\_features/README.md) and \[\`EXT\_structural\_metadata\`\](https://github.com/CesiumGS/glTF/blob/4bc06a1b608a44d15ccc9826f4a7bd4a7e696692/extensions/2.0/Vendor/EXT\_structural\_metadata/README.md) to associate points with metadata
> \* \`Feature ID by Index\` + property tables
> \* Styling is applied separately, e.g. with \[3D Tiles Declarative Styling\](https://github.com/CesiumGS/3d-tiles/tree/main/specification/Styling)
> \* This would include label rendering, anchor lines, etc.
> 
> \> \[!NOTE\]
> \> \* How does this relate to \[KHR\_billboard\](https://github.com/KhronosGroup/glTF/pull/2522)?
> 
> \### Lines
> 
> \* Use primitive \`mode: 1\` (\`LINES\`), \`mode: 2\` (\`LINE\_LOOP\`) or \`mode: 3\` (\`LINE\_STRIP\`)
> \* For the latter two, use \[\`EXT\_mesh\_primitive\_restart\`\](https://github.com/KhronosGroup/glTF/pull/2478) to batch multiple lines into a single primitive
> \* Use \[\`EXT\_mesh\_features\`\](https://github.com/CesiumGS/glTF/blob/proposal-EXT\_mesh\_features/extensions/2.0/Vendor/EXT\_mesh\_features/README.md) and \[\`EXT\_structural\_metadata\`\](https://github.com/CesiumGS/glTF/blob/4bc06a1b608a44d15ccc9826f4a7bd4a7e696692/extensions/2.0/Vendor/EXT\_structural\_metadata/README.md) to associate lines with metadata.
> \* \`Feature ID by Vertex\` + property tables.
> \* Styling is applied separately, e.g. with \[3D Tiles Declarative Styling\](https://github.com/CesiumGS/3d-tiles/tree/main/specification/Styling)
> \* This would include mitering, line width, dashed patterns, etc.
> 
> \### Polygons
> 
> \* Use primitive \`mode: 4\` (\`TRIANGLES\`) - polygons are pre-triangulated for performance reasons
> \* Use \[\`EXT\_mesh\_features\`\](https://github.com/CesiumGS/glTF/blob/proposal-EXT\_mesh\_features/extensions/2.0/Vendor/EXT\_mesh\_features/README.md) and \[\`EXT\_structural\_metadata\`\](https://github.com/CesiumGS/glTF/blob/4bc06a1b608a44d15ccc9826f4a7bd4a7e696692/extensions/2.0/Vendor/EXT\_structural\_metadata/README.md) to associate polygons with metadata.
> \* \`Feature ID by Vertex\` + property tables.
> \* Indicate which edges form the outline
> \* Option 1: extension that requires positions to be contiguous. See the \[legacy .vctr format\](https://github.com/CesiumGS/3d-tiles/tree/vctr/TileFormats/VectorData) and \[\`FB\_ngon\_encoding\`\](https://github.com/KhronosGroup/glTF/pull/1620)
> \* Option 2: extension that encodes edge indices separately, as in \[\`EXT\_mesh\_primitive\_edge\_visibility\`\](https://github.com/KhronosGroup/glTF/blob/96aa219b4099574a4f98dbfdbc52ba7c755e04ab/extensions/2.0/Vendor/EXT\_mesh\_primitive\_edge\_visibility/README.md) or \[\`CESIUM\_primitive\_outline\`\](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/CESIUM\_primitive\_outline/README.md)
> \* Styling is applied separately, e.g. with \[3D Tiles Declarative Styling\](https://github.com/CesiumGS/3d-tiles/tree/main/specification/Styling)
> \* This would include outline, fill pattern, etc.
> 
> \> \[!NOTE\]
> \> \* How to encode edges for polygons with holes?
> 
> \### Styling
> 
> \* Styling features would happen client side. Some examples:
> \* Points: labels, anchor lines, etc
> \* Lines: mitering, line width, dashed patterns, etc
> \* Polygons: outline, fill pattern, etc
> \* Styling would also define layering, ordering, and draping behavior
> \* \[3D Tiles Declarative Styling\](https://github.com/CesiumGS/3d-tiles/tree/main/specification/Styling) files could be provided alongside a tileset.json
> 
> \### Compression
> 
> \* From previous experiments, glTF + meshopt + gzip file size was on par with the \[legacy .vctr format\](https://github.com/CesiumGS/3d-tiles/tree/vctr/TileFormats/VectorData) + gzip
> \* Haven't compared against other formats like \[Mapbox Vector Tiles\](https://github.com/mapbox/vector-tile-spec), \[MapLibre Tiles\](https://github.com/maplibre/maplibre-tile-spec), or \[Geobuf\](https://github.com/mapbox/geobuf)
> 
> \### Demo
> 
> Here's a demo of some vector data (provided by Maxar) converted from GeoJSON to 3D Tiles - includes line and polygon features and runtime styling. No \_new\_ extensions and running in stock CesiumJS.
> 
> glTF files are using the following extensions:
> 
> \* \[\`EXT\_mesh\_features\`\](https://github.com/CesiumGS/glTF/blob/proposal-EXT\_mesh\_features/extensions/2.0/Vendor/EXT\_mesh\_features/README.md)
> \* \[\`EXT\_structural\_metadata\`\](https://github.com/CesiumGS/glTF/blob/4bc06a1b608a44d15ccc9826f4a7bd4a7e696692/extensions/2.0/Vendor/EXT\_structural\_metadata/README.md)
> \* \[\`EXT\_meshopt\_compression\`\](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT\_meshopt\_compression)
> 
> !\[Image\](https://github.com/user-attachments/assets/2773d6dd-9038-4790-8081-0b49bf246b44)

However, support for the Mapbox _Style Specification_ is not in progress currently, and I can’t offer an ETA on that. Cesium 3D Tiles styling will be supported on both 3D Tiles and MVT tilesets in CesiumJS, but is currently more limited in vector-specific features today, and may not cover every need in your Mapbox style JSON. We’d welcome feedback on your needs and any issues you run into, as issues on the CesiumJS repo or on 3D Tiles issue #825 above.

I would be curious which styling features (fill-color, fill-opacity, circle-radius, …) are most important to you? The logical expressions are important but to some extent that can be worked around with custom JavaScript code, where missing styling features cannot.

We’re aware that draping on terrain will be an important use case for vector data in CesiumJS, and intend to have a solution (for both MVT and 3D Tiles) that avoids pixelation/stretching on slopes. This may affect billboard and label rendering in the longer term, but for now I would just use BillboardCollection and LabelCollection as you suggest.

I don’t think I can suggest much on your proposed architecture, other than mentioning the CesiumJS roadmap goals above. If you do move rasterization server-side, I assume that reduces performance bottlenecks but does not address the distortion/stretching, correct?

---

<div class="post-metadata">

**Author:** ![zanzandora](https://sea2.discourse-cdn.com/cesium/user_avatar/community.cesium.com/zanzandora/32/28173_2.png) [@zanzandora](https://community.cesium.com/u/zanzandora)\
**Post date:** [May 2, 2026, 3:19pm UTC](https://community.cesium.com/t/best-practices-for-hybrid-maplibre-cesium-architecture-handling-mvt-terrain-and-complex-styling-in-production/46031/3 "2026-05-02T15:19:20Z")

</div>

Hi @donmccurdy,

Thank you so much for the detailed roadmap insights. It really helps define the technical boundaries for our project.

We have decided to **completely strip out POIs, labels, and icons** in our 3D environment. Therefore, billboard/label distortion on terrain is no longer a blocker for us.

I’d like to share some real-world feedback from our extensive prototyping with current workarounds, keeping in mind our strict infrastructure constraint: _I only have access to the Frontend and a local Middleware pipeline. I have zero access to the Database or Backend, and can only consume the existing MVT fetch API (_ I could discuss it with the PM, but that’s not very feasible… )

**1. Regarding the MVT → GeoJSON → Cesium (or Ion) approach:** I successfully built a Node.js pipeline to fetch our MVTs, parse them into GeoJSON, and load them into Cesium. However, it proved to be unviable for a production product for three reasons:

- **Performance Killer:** The parsed GeoJSON is flat by default. Applying `clampToGround: true` over 3D Terrain for this massive amount of vector data caused severe performance hits and unplayable lag.

- **Style Data Loss:** The transformation from MVT to GeoJSON strips away crucial properties linked to our Mapbox style (colors, complex categorical rules), making restyling natively in Cesium incredibly difficult.

- **Cesium ion Bottleneck:** Uploading massive GeoJSON files to Ion introduces quota/paid tier constraints that are not optimal for our current product model.

**2. Regarding the Hybrid (Client-side Canvas Draping) approach:** We experimented with the client-side hybrid method (using MapLibre GL JS to render vectors to a canvas and drape it over Cesium). It was highly unsuccessful and extremely laggy, even _before_ we loaded any 3D building models. Managing dual WebGL contexts client-side is simply too heavy.

**My Proposed Direction & Seeking Your Advice:** Given these severe constraints (No DB/BE access, massive lag with `clampToGround` GeoJSON, unplayable lag with Hybrid canvas draping), Im currently leaning toward building a **Server-Side Node.js Tile Proxy**. This middleware would fetch the MVT, rasterize it into PNG tiles on the fly, and serve them to Cesium’s `UrlTemplateImageryProvider`.

However, before we commit our resources to building and maintaining this server-side rasterization pipeline, I wanted to leave this open for your expertise.

- Is there any other Cesium-native workaround or optimization we might have missed for draping massive flat vectors over terrain without completely tanking the framerate?

- Or does the Tile Proxy approach sound like the only realistic path forward until native MVT support arrives?

We are incredibly excited about the native MVT support and will be keeping a close eye on PR #13404!

Thanks again for your time and guidance.

---

<div class="post-metadata">

**Author:** ![donmccurdy](https://sea2.discourse-cdn.com/cesium/user_avatar/community.cesium.com/donmccurdy/32/4683_2.png) [@donmccurdy](https://community.cesium.com/u/donmccurdy)\
**Post date:** [May 4, 2026, 2:14pm UTC](https://community.cesium.com/t/best-practices-for-hybrid-maplibre-cesium-architecture-handling-mvt-terrain-and-complex-styling-in-production/46031/4 "2026-05-04T14:14:14Z")

</div>

> Is there any other Cesium-native workaround or optimization we might have missed for draping massive flat vectors over terrain without completely tanking the framerate?

Not today, no. Draping vectors over terrain is currently very expensive, and does not yet scale well to large datasets. We’re actively working to improve this performance in the coming months. For progress, you can track this PR:

> <https://github.com/CesiumGS/cesium/pull/13325>
>
> \<p align="center"\>
> \<img src="https://github.com/user-attachments/assets/31f84…53c-eaa5-4e73-80b6-3734f73fd2d0" alt="image" width="49%" /\>
> \<img src="https://github.com/user-attachments/assets/48c81163-c4e9-4418-af48-f59fd4dbce63" alt="image" width="49%" /\>
> \</p\>
> 
> 
> \# Description
> 
> Note: This is still a very rough draft, just getting it to render for now. It will need cleanup, revisions, and refactoring.
> 
> Please read my notes to understand the implementation better!
> \[README.md\](https://github.com/user-attachments/files/26226099/README.md)
> 
> 
> This PR adds a \`gpuLookup\` rendering mode for glTF vector tiles using \`CESIUM\_mesh\_vector\`.
> 
> I'm currently keeping:
> \- route glTF vector content into \`VectorGltf3DTileContent\`
> \- decode it into \`BufferPointCollection\`, \`BufferPolylineCollection\`, and \`BufferPolygonCollection\`
> \- keep the normal collection renderer as the baseline / fallback path for better debug now. Turn on GPU lookup using:
> \`\`\`
> const tileset = await Cesium.Cesium3DTileset.fromUrl(url, {
> heightReference: Cesium.HeightReference.CLAMP\_TO\_TERRAIN,
> });
> 
> \`\`\`
> 
> What changes is that polylines and polygons can now render through an alternate tile local GPU lookup path.
> 
> \## Compare
> | Area | Earlier experiment branch | This PR |
> | --- | --- | --- |
> | Input pipeline | Terrain / GeoJSON-oriented experiment path | \`CESIUM\_mesh\_vector -\> VectorGltf3DTileContent -\> Buffer\*Collection\` |
> | Points | No special lookup path | Still use normal \`BufferPointCollection\` rendering |
> | Lines | Tile-local segment lookup on terrain surface UVs | Tile-local segment lookup on a generated tile surface patch |
> | Polygons | Edge segments + ray casting / cutFlags | regular polygon fill |
> | Carrier surface | Reused terrain tile mesh | Generated tile surface patch over the tile region |
> | Lookup domain | Tile-local terrain UV | Tile-local geographic UV from tile bounding region |
> 
> 
> \### My attempt
> 
> \- I first tried building GPU lookup data per decoded collection using a single local 2D plane. That worked structurally, but real datasets like \`sample-us-outline\` and \`sample-us-states\` are draped over large curved regions, so the collection-local plane assumption broke down. The result was that the lookup path immediately fell back to the normal geometry renderer. This PR moves lookup generation to tile-local surface / UV space instead.
> 
> \- Polygons: I tried to carry over ray-casting / cut-flag style idea from the earlier experiment, but it rendered very slowly on our dataset for reasons, I need to spend more time to investigate. Polygon is currently using regular polygon fills, which is the approach we’ve decided to go with.
> 
> \### Future work / Current problem
> 
> \- Polyline: visual quality and performance is bad
> \- Refactor, Refine better \`Buffer\*Collection\` pipeline or API
> 
> \## Issue number and link
> 
> 
> 
> 
> 
> \## Testing plan
> 
> \<details\>
> \<summary\>Example Sandcastle code:\</summary\>
> 
> \`\`\`js
> const viewer = new Cesium.Viewer("cesiumContainer");
> viewer.scene.debugShowFramesPerSecond = true;
> 
> const url = "http://localhost:8082/sample-us-states.tileset.json";
> 
> const tileset = await Cesium.Cesium3DTileset.fromUrl(url, {
> heightReference: Cesium.HeightReference.CLAMP\_TO\_TERRAIN,
> });
> 
> window.tileset = tileset;
> viewer.scene.primitives.add(tileset);
> viewer.zoomTo(tileset);
> \`\`\`
> 
> \</details\>
> 
> 
> 
> 
> \# Author checklist
> 
> \- \[\] I have submitted a Contributor License Agreement
> \- \[\] I have added my name to \`CONTRIBUTORS.md\`
> \- \[\] I have updated \`CHANGES.md\` with a short summary of my change
> \- \[\] I have added or updated unit tests to ensure consistent code coverage
> \- \[\] I have updated the inline documentation, and included code examples where relevant
> \- \[\] I have performed a self-review of my code

This still leaves the Mapbox style question, of course; you would need to evaluate the feasibility of implementing your specific style needs in available APIs like [Cesium3DTileStyle](https://cesium.com/learn/cesiumjs/ref-doc/Cesium3DTileStyle.html).

* * *

> This middleware would fetch the MVT, rasterize it into PNG tiles on the fly, and serve them to Cesium’s `UrlTemplateImageryProvider` …

I would just caution that typical server hardware is not necessarily much stronger than the client devices it’s serving to, and (unless you’re paying significantly more) may not have access to a GPU as the client device would. Server-side graphics rendering (as opposed to HTML rendering) can become very expensive, depending on the amount of traffic/users you expect. The more that you can cache the results the better, of course.

---

<div class="post-metadata">

**Author:** ![zanzandora](https://sea2.discourse-cdn.com/cesium/user_avatar/community.cesium.com/zanzandora/32/28173_2.png) [@zanzandora](https://community.cesium.com/u/zanzandora)\
**Post date:** [May 6, 2026, 3:25am UTC](https://community.cesium.com/t/best-practices-for-hybrid-maplibre-cesium-architecture-handling-mvt-terrain-and-complex-styling-in-production/46031/5 "2026-05-06T03:25:30Z")

</div>

Hi @donmccurdy,

Thanks for the reality check on server costs! We’ve dropped the live server-rendering idea and are pivoting entirely to 100% static/client-side approaches. We’re currently weighing 2 paths:

**Path A: Shared WebGL Context (MapLibre + Three.js)** MapLibre handles the vector basemap natively. We share its WebGL context with Three.js and use the `3d-tiles-renderer` to stream our massive BIM models over the map ( This is currently just a conceptual theory for me, and I am not sure if it holds up at scale). Example: [https://maplibre.org/maplibre-gl-js/docs/examples/add-3d-tiles-using-threejs/#\_\_tabbed\_1\_1](https://maplibre.org/maplibre-gl-js/docs/examples/add-3d-tiles-using-threejs/#__tabbed_1_1)

**Path B: Cesium + Single High-Res Image** We render the vector basemap into one massive, high-res image locally and upload it to Cesium ion, letting Ion handle the tiling and terrain draping. For the 3D buildings, we pre-bake the terrain heights and output static `.i3dm` tilesets.

Path A lets us keep our complex vector styles intact, while B lean fully into Cesium’s core strengths. Have you seen teams succeed at scale with Path A’s shared-context approach, or do you think B is the safer bet to avoid hidden WebGL bottlenecks?

Thanks again for your time!

---

<div class="post-metadata">

**Author:** ![donmccurdy](https://sea2.discourse-cdn.com/cesium/user_avatar/community.cesium.com/donmccurdy/32/4683_2.png) [@donmccurdy](https://community.cesium.com/u/donmccurdy)\
**Post date:** [May 6, 2026, 3:00pm UTC](https://community.cesium.com/t/best-practices-for-hybrid-maplibre-cesium-architecture-handling-mvt-terrain-and-complex-styling-in-production/46031/6 "2026-05-06T15:00:34Z")

</div>

I don’t think I have a preference between (A) and (B) myself, but perhaps others might. I believe either could work.

WebGL certainly _can_ efficiently render vector tiles (that’s what we’re working toward as well, in CesiumJS!), so I think the performance costs you’re seeing with the background rasterization are more specific to that setup and some unknown details in the client-side MVT → MapLibre → image → CesiumJS pipeline. So MapLibre+three.js could in theory work well here, but I don’t have experience combining the two in production.

---

<div class="post-metadata">

**Author:** ![zanzandora](https://sea2.discourse-cdn.com/cesium/user_avatar/community.cesium.com/zanzandora/32/28173_2.png) [@zanzandora](https://community.cesium.com/u/zanzandora)\
**Post date:** [May 7, 2026, 3:12am UTC](https://community.cesium.com/t/best-practices-for-hybrid-maplibre-cesium-architecture-handling-mvt-terrain-and-complex-styling-in-production/46031/7 "2026-05-07T03:12:10Z")

</div>

> [@donmccurdy](#):
>
> So MapLibre+three.js could in theory work well here, but I don’t have experience combining the two in production.

I know that manually handling the tile loading and memory management in Three.js will be a tough challenge without CesiumJS doing the heavy lifting under the hood, but sharing that single WebGL context seems like the only viable path to hit our performance goals right now.
