Strategy for tile-prefetching during camera-movement
JannikGM opened this issue ยท 37 comments
We often use flyTo
, but maplibre doesn't seem to be smart enough to prefetch tiles along the animation path.
The same is true for user mouse-movement (pan / zoom / rotate).
Ideally, the map would extrapolate where the map is going to render in the near future, so it can already load tiles.
When combined with a small delay
in the AnimationOptions
this could be used to prefetch before the movement even starts.
@AbelVM had recently shown a "hack" on maplibre Slack, which prefetches neighbouring tiles in the browser-cache: https://github.com/AbelVM/mapworkbox
Even this naive brute-force strategy (which doesn't respect camera movement direction) shows that better prefetching can have a performance impact.
The target of the PoC was not to provide a final feature, but to test the feasibility and benchmark the WorkBox dynamic precaching vs vanilla one. That's why the tile logic is that simple, as it only takes into account pan & zoom prediction in a coarse way ๐คท
Regardless that whiny side note ๐คฃ , using dynamic precaching for flyTo
, panTo
, etc. makes total sense to me, as it lowers the source loading time a 35% (as per my benchmarks) it might lead to a way smoother animation and the logic to implement that is quite simple
I have started working on an approach to add this feature to MapLibre. It is a bit of a mess because service workers and bundlers are still getting to know each other and I am a vanilla-JS guy. But that's just dev stuff, not the code itself. ๐ค
The starting idea is to attach a service worker that will remain asleep till the user uses any "moving" function where the destination is provided, so we can build all the logic (jumpTo
, easeTo
and flyTo
). IMHO, it makes no sense to prefetch tiles for simple and short-ranged user interactions like pan, zoom or rotate as the common tiles fetching and caching might be enough, and the times from start to end of movement is not enough to get noticeable performance improvements, if any.
The very first version of the precaching is intended to hijack those moving functions and start precaching as soon as they are called. And which tiles are supposed to be precached (eventually)?
- All the tiles that contain the center of the map during the animation
- The 3 sibling tiles of those center tiles
- All the tiles within the final viewport, once the camera has arrived to the destination
This way, the "focus area" of the map will be perfectly loaded and rendered during the animation, and the final scenario will be ready to welcome the user camera as it arrives.
Taking into account the pitch and bearing at every frame of the animation to estimate the tiles in the viewport would imply heavy calculations that might nullify the precaching advantage, so they are out of my scope in this first version
I think this is a great idea. I know that our style is probably too complicated to be rendered fast, and it won't show up while animating the flyto, but it would be great to prioritize the final tiles at the destination. For us at least...
First swerve: moving form precaching
(service workers) to preloading
(web workers).
We don't need to manage the cache life-cycle of the tiles to be used during and at the end of the camera movement, we just need to preload them fast enough for them to be locally available when requested by the camera logic.
This change:
- Avoids the need for an external, physical, file for the service worker code (w3c/ServiceWorker#578), keeping MapLibreGL JS library as a single file (plus CSS, you know)
- Allows us to drop the
workbox
dependency - Simplifies the logic and the code
Regarding the note on prioritizing the tiles of the final scenario vs. the fly-by ones, it's an easy change. But, maybe, I'd just split the tiles list and send two different minions to preload them without interfering
News!
- Instead of messing with the library code, I've built a tiny plugin for this functionality. It adds
cached
versions ofpanTo
,zoomTo
,jumpTo
,easeTo
andflyTo
. Just 7.7KB once bundled ๐ค - The logic might be implemented within MapLibre, but it might be somehow... dirty, IMHO.
- Web workers fulfill our needs, no need to drive us crazy with service workers and the implicit complexity
- This first version will only preload the final scenario, not the tiles for the animation. I've made some tests and trying to precache the animation might be a futile task as the animation itself might be faster than the preloading (of a potentially huge number of tiles) ๐
- Performance:
- The tiles are preloaded way before the movement has ended
- All the final scenario tiles that fall within the viewport are hitting cache! MapLibre still requests some extra tiles out of the viewport to take into account minor pan movements by the user without new requests & repaint, so I might need to add some buffer to the final viewport logic.
- There is still a minor bug with paths in Firefox, but it's just a matter of time I fix it.
Nice work! I would be interested to know more about this.
If there was a flag in maplibre to enable this it might be useful to others, in an opt-in mode...?
I don't know. In any case, if you decide to send a PR please do it over the typescript branch in order to avoid conflicts...
So, that's it ๐ค I have built a tiny experimental plugin for tiles preloading at
https://github.com/AbelVM/maplibre-preload
Please read the caveats and final thoughts
as this is not a golden hammer (definitely not to be included in the main lib, IMHO)
This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.
Should this plugin be added to https://github.com/maplibre/maplibre-gl-js-docs/blob/main/docs/data/plugins.json and/or https://github.com/maplibre/awesome-maplibre before closing this issue?
@AbelVM please decide if and how you would like to publish the tiny plugin you wrote...
There is a pending PR in the Awesome List, actually: maplibre/awesome-maplibre#4
But adding it to the plugins list might give it more visibility and maybe * someone * helps turning it prod-quality and adding it to main branch ๐
Upstream mapbox is getting this feature right now: mapbox/mapbox-gl-js#11328
Some thoughts on their proposed implementation:
- I don't like how they emulate 60FPS to find all touched tiles.
- I do like that they have some preload function to preload specific tiles.
- I strongly agree with mourners first comment, that prefetching and actually moving should be separated.
I think the most valuable UX related to this feature is that the map is "there" when you arrive to the destination.
It's nice that the while the flight is animating the map is also presented, but I see it as a lesser improvement to UX, at least from my point of view.
I'm not sure how preloading and moving will work in terms of splitting them, feels odd to me - i.e. write something like: map.floytopreload(...)
and then map.flyto(...)
doesn't feel like a good API but I might be missing a use case - my use case is that the user select a search results and I'm using flyTo to move to that position so I don't want the user to wait before the movement starts.
My 2 cents: if we can use a flag in the flyToOptions to signal the map to try and get the destination tiles first and only after that which ever tiles are missing it would be a great benefit.
My tiny plugin already has that kind of pattern, you can call map.cachedFlyTo(...)
to just preload the tiles or map.cachedFlyTo(..., {run:true})
to preload and trigger the flyTo
method.
It would be easy to overload the original methods with (an improved version of) my code, I just tried to be the least invasive as possible in the plugin
It's nice that the while the flight is animating the map is also presented, but I see it as a lesser improvement to UX, at least from my point of view. I'm not sure how preloading and moving will work in terms of splitting them, feels odd to me - i.e. write something like:
map.floytopreload(...)
and thenmap.flyto(...)
doesn't feel like a good API but I might be missing a use case - my use case is that the user select a search results and I'm using flyTo to move to that position so I don't want the user to wait before the movement starts.
I think it will be very useful UX to precache the flight path, and not just the destination. If we were to animate from London to New York, it will be quite disorienting if we're unable to see much of the zoom-out, travel and zoom-in between these two destinations.
For reference, OpenLayers has a very neat implementation:
https://openlayers.org/en/latest/examples/preload.html
Hmmm... It looks like they preload lower (z-n) resolution tiles if the z tiles are not cached:
Worths a look ๐ค
Hi, this plugin sounds like something I need. My app has few flyto points on the map, but when working on 4K display it is very laggy. However I can't make your tiny plugin to work with my setup. I set up my map with reactmap-gl and maplibre. How should I install maplibre-preload to work with reactmap-gl? I import the built package to index.js or App.js but I don't see cachedFlyTo neither under map nor map.getMap(). Can you help?
Can you share a stackblitz or jsbin?
Here is a stackblitz: https://stackblitz.com/edit/github-kut1lo?file=src/index.js
I simplified it for testing purpose. I tried importing bundled module and installing plugin as node_module with:
npm i file:../maplibre-preload
and then importing it to App.js
Neither works. How should I do it?
Can we close this issue? I think the plugin is a good enough solution at this point...
I think we should still have this feature natively in maplibre.
The plugin only primes the browser cache, however, this will not work if the cache is disabled or the server disabled caching (such as people who generate tiles dynamically). For these cases, it will double the server workload and the bandwidth to download tiles.
A native implementation would only have to fetch tiles once, and it could also start preprocessing tiles right away (creating necessary GL buffers etc.). This would remove a lot of the micro-stutter people observe when moving the camera.
The plugin is a nice hack for some use-cases, but prefetching of tiles should be a core feature of maplibre.
I don't think maplibre necessarily has to handle finding which tiles will be required, but there should be functions to prefetch and evict tiles based on an area or tile-id, so the existing plugin could be improved.
I totally agree with @JannikGM , we need to smooth the user experience of flyTo
(mainly) actions
As previously mentioned, the strategy used by OpenLayers worths a look
- https://openlayers.org/en/latest/examples/preload.html
- https://github.com/openlayers/openlayers/blob/10fb55b9e620946551195d2cf52f9d320f701c30/src/ol/renderer/webgl/TileLayer.js#L313
In order to save bandwidth and load times, looks like they preload tiles at lower zoom levels for the target and crossing area and over-zoom them to the current map zoom, and once arrived, gracefully swapping them when final tiles of the target area are loaded.
Fair enough.
I would recommend pitching a design for this to facilitate for the "main" use cases as the default behavior, and allow for more configuration for edge and uncommon use cases.
For example I would consider only pre-fetching the target tiles as default and allow the developer a way to configure the per-fetch in a different way...
I've seen that pre-fetching becomes more important when we are talking about the terrain due to camera movement.
Cc @prozessor13
Hey folks I wanted to share what the experience looks like right now on a good internet connection for the example where we fly to specific locations based on the scroll position here
https://maplibre.org/maplibre-gl-js/docs/examples/scroll-fly-to/
Screenshare.-.2023-10-05.12.17.15.PM.webm
For these map story-telling use cases (even without terrain or pitched camera angles) maplibre-gl-js struggles to deliver a good user experience out of the box.
Regardless of your bandwidth, concurrent connections to the same server are limited to 6 in your browser, so, if you pan/zoom too fast, you're just sending and canceling lots of requests on the fly, as the tiles are still downloading when they are tagged as not needed (out of the viewport) and their requests canceled.
So, yep, this is a big issue that we should look into imho.
How do I set this pluginup in my typscript react app where I have my map initalized in another class?
're just sending and canceling lots of requests on the fly, as the ti
Only on http 1.X I guess....
Has there been any progress on this issue? I also don't get the plugin to work properly in my environment - only "Movement has finished before preloading" gets triggered.
This project is just a PoC, quite naive. If there's a real interest on this feature, we should get serious, study the OpenLayers strategy (as it looks promising), and push this feature to MapLibre itself.
Any opinion @HarelM ?
There are strong forces to keep the bundle size small and so if this is possible using a plugin without a lot of "hacking" I think it can be a good solution.
@AbelVM approach is naive, but anyone with the interest of improving this can add a PR to the specified repo or create a different plugin.
Given the above options and the community's engagement, I reluctant to say that there's a real interest here.
Having said that, if the code changes are small and the value is high (without tons of configurations) I think we can entertain the idea of adding this to this library.
There are my 2 cents at least.
For my usecase (Storymaps) I need this feature, otherwise flyTo is completly useless. As already mentioned the example https://maplibre.org/maplibre-gl-js/docs/examples/scroll-fly-to/ does not really work smoothly without precaching.
My workaround will be to preload the tiles programmatically when the site loads as I have an already specified "flight-path".
If I read @AbelVM's code correctly, it is enough to just fetch the URLs of the tiles so the browser has them cached and not pass them already to maplibre.
Having said that, if the code changes are small and the value is high (without tons of configurations) I think we can entertain the idea of adding this to this library.
FWIW, IMO, this should be considered integral to MapLibre. One could even say that the flyTo feature right now is half implemented since it doesn't really work for most users. The cache behaviour is crucial to the UX.
But I do take your point on the bundle size.
Looking at their code will infringe their copyright rules. I would advise against it.
Instead of Mapbox, check OpenLayers approach:
As part of the globe branch, we have moved all the tile logic to separate files so that we will be able to improve and change strategies better.
I believe that this will help us better define strategies for tile loading, maybe we should even consider a hook in maplibre to allow customizing the tile loading logic to allow people to define different algorithms to solve this issue.
In any case, the first step is to contain the logic in one place that will allow to modify it without affecting other places in the code, which is being made in the globe branch.
Happy to help, as soon as I'm familiar with the new tiles code