diff --git a/CHANGELOG.md b/CHANGELOG.md index 976a70946e..6e23533ec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,9 @@ ### 🐞 Bug fixes -- Correct declared return type of `Map.getLayer()` and `Style.getLayer()` to be `StyleLayer | undefined` to match the documentation. -- Correct type of `Map.addLayer()` and `Style.addLayer()` to allow adding a layer with an embedded source, matching the documentation. +- Correct declared return type of `Map.getLayer()` and `Style.getLayer()` to be `StyleLayer | undefined` to match the documentation ([#2969](https://github.com/maplibre/maplibre-gl-js/pull/2969)) +- Correct type of `Map.addLayer()` and `Style.addLayer()` to allow adding a layer with an embedded source, matching the documentation ([#2966](https://github.com/maplibre/maplibre-gl-js/pull/2966)) +- Throttle map resizes from ResizeObserver to reduce flicker ([#2986](https://github.com/maplibre/maplibre-gl-js/pull/2986)) - _...Add new stuff here..._ ## 3.3.0 diff --git a/src/ui/map.test.ts b/src/ui/map.test.ts index 955fbb2766..2bd2dd3f34 100755 --- a/src/ui/map.test.ts +++ b/src/ui/map.test.ts @@ -861,21 +861,30 @@ describe('Map', () => { const map = createMap(); - const spyA = jest.spyOn(map, '_update'); - const spyB = jest.spyOn(map, 'resize'); + const updateSpy = jest.spyOn(map, '_update'); + const resizeSpy = jest.spyOn(map, 'resize'); // The initial "observe" event fired by ResizeObserver should be captured/muted // in the map constructor observerCallback(); - expect(spyA).not.toHaveBeenCalled(); - expect(spyB).not.toHaveBeenCalled(); + expect(updateSpy).not.toHaveBeenCalled(); + expect(resizeSpy).not.toHaveBeenCalled(); - // Following "observe" events should fire a resize / _update + // The next "observe" event should fire a resize / _update + + observerCallback(); + expect(updateSpy).toHaveBeenCalled(); + expect(resizeSpy).toHaveBeenCalledTimes(1); + // Additional "observe" events should be throttled + observerCallback(); + observerCallback(); + observerCallback(); observerCallback(); - expect(spyA).toHaveBeenCalled(); - expect(spyB).toHaveBeenCalled(); + expect(resizeSpy).toHaveBeenCalledTimes(1); + await new Promise((resolve) => { setTimeout(resolve, 100); }); + expect(resizeSpy).toHaveBeenCalledTimes(2); }); test('width and height correctly rounded', () => { diff --git a/src/ui/map.ts b/src/ui/map.ts index e967bf950b..0ba0ee8976 100644 --- a/src/ui/map.ts +++ b/src/ui/map.ts @@ -25,6 +25,7 @@ import {RGBAImage} from '../util/image'; import {Event, ErrorEvent, Listener} from '../util/evented'; import {MapEventType, MapLayerEventType, MapMouseEvent, MapSourceDataEvent, MapStyleDataEvent} from './events'; import {TaskQueue} from '../util/task_queue'; +import {throttle} from '../util/throttle'; import {webpSupported} from '../util/webp_supported'; import {PerformanceMarkers, PerformanceUtils} from '../util/performance'; import {Source, SourceClass} from '../source/source'; @@ -638,15 +639,17 @@ export class Map extends Camera { if (typeof window !== 'undefined') { addEventListener('online', this._onWindowOnline, false); let initialResizeEventCaptured = false; + const throttledResizeCallback = throttle((entries: ResizeObserverEntry[]) => { + if (this._trackResize && !this._removed) { + this.resize(entries)._update(); + } + }, 50); this._resizeObserver = new ResizeObserver((entries) => { if (!initialResizeEventCaptured) { initialResizeEventCaptured = true; return; } - - if (this._trackResize) { - this.resize(entries)._update(); - } + throttledResizeCallback(entries); }); this._resizeObserver.observe(this._container); } diff --git a/src/util/throttle.ts b/src/util/throttle.ts index fa210e02ff..17c90c8df9 100644 --- a/src/util/throttle.ts +++ b/src/util/throttle.ts @@ -1,21 +1,25 @@ /** * Throttle the given function to run at most every `period` milliseconds. */ -export function throttle(fn: () => void, time: number): () => ReturnType { +export function throttle void>(fn: T, time: number): (...args: Parameters) => ReturnType { let pending = false; let timerId: ReturnType = null; + let lastCallContext = null; + let lastCallArgs: Parameters; const later = () => { timerId = null; if (pending) { - fn(); + fn.apply(lastCallContext, lastCallArgs); timerId = setTimeout(later, time); pending = false; } }; - return () => { + return (...args: Parameters) => { pending = true; + lastCallContext = this; + lastCallArgs = args; if (!timerId) { later(); }