diff --git a/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java b/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java
index f9ee08d..8ac948f 100644
--- a/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java
+++ b/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java
@@ -6,12 +6,16 @@ import static org.openstreetmap.josm.tools.I18n.tr;
 import static org.openstreetmap.josm.tools.I18n.trc;
 import static org.openstreetmap.josm.tools.I18n.trn;
 
+import java.awt.BasicStroke;
+import java.awt.Color;
 import java.awt.Cursor;
+import java.awt.Graphics2D;
 import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.event.InputEvent;
 import java.awt.event.KeyEvent;
 import java.awt.event.MouseEvent;
+import java.awt.geom.Path2D;
 import java.awt.geom.Point2D;
 import java.util.Collection;
 import java.util.Collections;
@@ -33,6 +37,7 @@ import org.openstreetmap.josm.command.MoveCommand;
 import org.openstreetmap.josm.command.RotateCommand;
 import org.openstreetmap.josm.command.ScaleCommand;
 import org.openstreetmap.josm.command.SequenceCommand;
+import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.SystemOfMeasurement;
 import org.openstreetmap.josm.data.UndoRedoHandler;
 import org.openstreetmap.josm.data.coor.EastNorth;
@@ -44,6 +49,7 @@ import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.WaySegment;
 import org.openstreetmap.josm.data.osm.visitor.AllNodesVisitor;
 import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer;
+import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
 import org.openstreetmap.josm.data.preferences.BooleanProperty;
 import org.openstreetmap.josm.data.preferences.CachingProperty;
 import org.openstreetmap.josm.gui.ExtendedDialog;
@@ -54,6 +60,7 @@ import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
 import org.openstreetmap.josm.gui.SelectionManager;
 import org.openstreetmap.josm.gui.SelectionManager.SelectionEnded;
 import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.MapViewPaintable;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.gui.util.KeyPressReleaseListener;
@@ -77,7 +84,7 @@ import org.openstreetmap.josm.tools.Utils;
  * On Mac OS X, Ctrl + mouse button 1 simulates right click (map move), so the
  * feature "selection remove" is disabled on this platform.
  */
-public class SelectAction extends MapMode implements ModifierExListener, KeyPressReleaseListener, SelectionEnded {
+public class SelectAction extends MapMode implements ModifierExListener, KeyPressReleaseListener, SelectionEnded, MapViewPaintable {
 
     private static final String NORMAL = /* ICON(cursor/)*/ "normal";
 
@@ -197,6 +204,9 @@ public class SelectAction extends MapMode implements ModifierExListener, KeyPres
      * set would have to be checked.
      */
     private transient OsmPrimitive currentHighlight;
+    // Whether currentHighlight was set via OsmPrimitive.setHighlighted (merge-mode drag).
+    // Only then does it need to be unset again when the highlight changes.
+    private boolean dataLayerHighlight;
 
     /**
      * Create a new SelectAction
@@ -218,6 +228,7 @@ public class SelectAction extends MapMode implements ModifierExListener, KeyPres
         mv.addMouseMotionListener(this);
         mv.setVirtualNodesEnabled(Config.getPref().getInt("mappaint.node.virtual-size", 8) != 0);
         drawTargetHighlight = Config.getPref().getBoolean("draw.target-highlight", true);
+        mv.addTemporaryLayer(this); // draws the hover highlight without invalidating the data layer
         initialMoveDelay = Config.getPref().getInt("edit.initial-move-delay", 200);
         initialMoveThreshold = Config.getPref().getInt("edit.initial-move-threshold", 5);
         repeatedKeySwitchLassoOption = Config.getPref().getBoolean("mappaint.select.toggle-lasso-on-repeated-S", true);
@@ -242,6 +253,7 @@ public class SelectAction extends MapMode implements ModifierExListener, KeyPres
         map.keyDetector.removeModifierExListener(this);
         map.keyDetector.removeKeyListener(this);
         removeHighlighting();
+        mv.removeTemporaryLayer(this);
         virtualManager.clear();
     }
 
@@ -271,6 +283,11 @@ public class SelectAction extends MapMode implements ModifierExListener, KeyPres
      * @return {@code true} if repaint is required
      */
     private boolean giveUserFeedback(MouseEvent e, int modifiers) {
+        if (mv.isPanning()) {
+            // Don't hover-highlight while panning: a highlight change invalidates the
+            // data layer and forces a full re-render, breaking the pan-reuse path.
+            return false;
+        }
         Optional<OsmPrimitive> c = Optional.ofNullable(
                 mv.getNearestNodeOrWay(e.getPoint(), mv.isSelectablePredicate, true));
 
@@ -380,7 +397,10 @@ public class SelectAction extends MapMode implements ModifierExListener, KeyPres
         if (currentHighlight == null) {
             return needsRepaint;
         }
-        currentHighlight.setHighlighted(false);
+        if (dataLayerHighlight) {
+            currentHighlight.setHighlighted(false);
+            dataLayerHighlight = false;
+        }
         currentHighlight = null;
         return true;
     }
@@ -388,16 +408,58 @@ public class SelectAction extends MapMode implements ModifierExListener, KeyPres
     private boolean repaintIfRequired(OsmPrimitive newHighlight) {
         if (!drawTargetHighlight || Objects.equals(currentHighlight, newHighlight))
             return false;
-        if (currentHighlight != null) {
+        // The highlighted primitive is drawn by this action's temporary layer (see paint),
+        // not via OsmPrimitive.setHighlighted, which would invalidate the data layer.
+        if (dataLayerHighlight) {
             currentHighlight.setHighlighted(false);
-        }
-        if (newHighlight != null) {
-            newHighlight.setHighlighted(true);
+            dataLayerHighlight = false;
         }
         currentHighlight = newHighlight;
         return true;
     }
 
+    /**
+     * Draws the currently highlighted (hovered) primitive as an overlay on top of the map.
+     * This replaces the old highlight via {@link OsmPrimitive#setHighlighted}, which
+     * invalidated the data layer on every feature crossing and forced a full re-render.
+     */
+    @Override
+    public void paint(Graphics2D g, MapView mv, Bounds bbox) {
+        OsmPrimitive p = currentHighlight;
+        if (p == null || !drawTargetHighlight) {
+            return;
+        }
+        Color highlight = PaintColors.HIGHLIGHT.get();
+        Color transparent = new Color(highlight.getRed(), highlight.getGreen(), highlight.getBlue(), 100);
+        if (p instanceof Node) {
+            Point2D pt = mv.getPoint2D((Node) p);
+            int radius = Config.getPref().getInt("mappaint.highlight.radius", 7) + 4;
+            int x = (int) Math.round(pt.getX());
+            int y = (int) Math.round(pt.getY());
+            g.setColor(transparent);
+            g.fillOval(x - radius, y - radius, 2 * radius, 2 * radius);
+        } else if (p instanceof Way) {
+            Path2D path = new Path2D.Double();
+            boolean first = true;
+            for (Node n : ((Way) p).getNodes()) {
+                if (n == null || n.isIncomplete()) {
+                    continue;
+                }
+                Point2D pt = mv.getPoint2D(n);
+                if (first) {
+                    path.moveTo(pt.getX(), pt.getY());
+                    first = false;
+                } else {
+                    path.lineTo(pt.getX(), pt.getY());
+                }
+            }
+            g.setColor(transparent);
+            g.setStroke(new BasicStroke(Config.getPref().getInt("mappaint.highlight.width", 4) + 4,
+                    BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+            g.draw(path);
+        }
+    }
+
     /**
      * Look, whether any object is selected. If not, select the nearest node.
      * If there are no nodes in the dataset, do nothing.
@@ -531,6 +593,7 @@ public class SelectAction extends MapMode implements ModifierExListener, KeyPres
             if (p != null) {
                 p.setHighlighted(true);
                 currentHighlight = p;
+                dataLayerHighlight = true;
                 needsRepaint = true;
             }
             mv.setNewCursor(getCursor(p), this);
diff --git a/src/org/openstreetmap/josm/gui/MapMover.java b/src/org/openstreetmap/josm/gui/MapMover.java
index 86cbaa8..887a7a1 100644
--- a/src/org/openstreetmap/josm/gui/MapMover.java
+++ b/src/org/openstreetmap/josm/gui/MapMover.java
@@ -21,7 +21,6 @@ import org.openstreetmap.josm.actions.mapmode.SelectAction;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.preferences.BooleanProperty;
 import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
-import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
 import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
@@ -244,7 +243,8 @@ public class MapMover extends MouseAdapter implements Destroyable {
         }
         nc.resetCursor(this);
         mousePosMoveStart = null;
-        MainApplication.getLayerManager().getLayers().forEach(Layer::invalidate);
+        // Deliberately do not invalidate the layers here: a pan only moved the view, so
+        // the last frame's composite is still valid and the pan-reuse buffer stays alive.
     }
 
     /**
diff --git a/src/org/openstreetmap/josm/gui/MapView.java b/src/org/openstreetmap/josm/gui/MapView.java
index f4034e7..10320f1 100644
--- a/src/org/openstreetmap/josm/gui/MapView.java
+++ b/src/org/openstreetmap/josm/gui/MapView.java
@@ -69,6 +69,7 @@ import org.openstreetmap.josm.gui.layer.MapViewPaintable;
 import org.openstreetmap.josm.gui.layer.MapViewPaintable.LayerPainter;
 import org.openstreetmap.josm.gui.layer.MapViewPaintable.MapViewEvent;
 import org.openstreetmap.josm.gui.layer.MapViewPaintable.PaintableInvalidationEvent;
+import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
 import org.openstreetmap.josm.gui.layer.MapViewPaintable.PaintableInvalidationListener;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.gui.layer.markerlayer.PlayHeadMarker;
@@ -236,9 +237,13 @@ LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener {
 
     private transient BufferedImage nonChangedLayersBuffer;
     private transient BufferedImage offscreenBuffer;
-    // Layers that wasn't changed since last paint
-    private final transient List<Layer> nonChangedLayers = new ArrayList<>();
     private int lastViewID;
+    // View the composite in nonChangedLayersBuffer was painted at. Needed to reuse the
+    // buffer for a translated view; lastViewID only detects equality, not the pan delta.
+    private double lastPaintedScale;
+    private EastNorth lastPaintedCenter;
+    // The layer set the composite was painted with; hide/show/add/remove changes it.
+    private List<Layer> lastPaintedLayers;
     private final AtomicBoolean paintPreferencesChanged = new AtomicBoolean(true);
     private Rectangle lastClipBounds = new Rectangle();
     private transient MapMover mapMover;
@@ -491,6 +496,14 @@ LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener {
         }
     }
 
+    /**
+     * Determines if the map is currently being panned (right-button drag in progress).
+     * @return {@code true} if a pan is in progress
+     */
+    public boolean isPanning() {
+        return mapMover != null && mapMover.movementInProgress();
+    }
+
     /**
      * Draw the component.
      */
@@ -551,17 +564,60 @@ LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener {
             }
         }
 
-        boolean canUseBuffer = !paintPreferencesChanged.getAndSet(false)
-                && nonChangedLayers.size() <= nonChangedLayersCount
+        boolean paintPrefsChanged = paintPreferencesChanged.getAndSet(false);
+        boolean sameLayerSet = lastPaintedLayers != null && lastPaintedLayers.equals(visibleLayers);
+        boolean canUseBuffer = !paintPrefsChanged
+                && sameLayerSet
+                && nonChangedLayersCount == visibleLayers.size()
                 && lastViewID == getViewID()
-                && lastClipBounds.contains(g.getClipBounds())
-                && nonChangedLayers.equals(visibleLayers.subList(0, nonChangedLayers.size()));
+                && lastClipBounds.contains(g.getClipBounds());
+
+        // Pan-reuse: when the view moved at constant zoom, nonChangedLayersBuffer holds the
+        // complete composite of all visible layers (copied back below), so the next frame is
+        // the old one shifted by the pan delta plus the newly exposed strip. Layers
+        // invalidated since the last frame (e.g. freshly loaded imagery tiles) are only
+        // repainted inside the strip; the rest is painted when the pan stops.
+        boolean viewMoved = lastViewID != getViewID();
+        boolean scaleOk = Utils.equalsEpsilon(getScale(), lastPaintedScale);
+        boolean lastClipFull = lastClipBounds.contains(0, 0, width, height);
+        boolean canReusePanned = viewMoved
+                && !paintPrefsChanged
+                && sameLayerSet
+                && scaleOk
+                && lastClipFull
+                && nonChangedLayersBuffer != null
+                && nonChangedLayersBuffer.getWidth() == width && nonChangedLayersBuffer.getHeight() == height;
+        double panDX = 0;
+        double panDY = 0;
+        if (canReusePanned) {
+            EastNorth center = getCenter();
+            panDX = (lastPaintedCenter.east() - center.east()) / lastPaintedScale;
+            panDY = (center.north() - lastPaintedCenter.north()) / lastPaintedScale;
+            // Sub-pixel movements (e.g. a projection change) fall back to the normal path
+            canReusePanned = Math.abs(panDX) < getWidth() && Math.abs(panDY) < getHeight()
+                    && (Math.abs(panDX) >= 0.5 || Math.abs(panDY) >= 0.5);
+        }
+        if (Config.getPref().getBoolean("mappaint.debug.pan-reuse", false)) {
+            Logging.info("PANREUSE " + (canReusePanned ? "reuse" : "full")
+                    + " canUse=" + canUseBuffer
+                    + " prefs=" + paintPrefsChanged
+                    + " count=" + nonChangedLayersCount + '/' + visibleLayers.size()
+                    + " layerSet=" + sameLayerSet
+                    + " buf=" + (nonChangedLayersBuffer != null
+                            ? nonChangedLayersBuffer.getWidth() + "x" + nonChangedLayersBuffer.getHeight() : "null")
+                    + " size=" + width + "x" + height
+                    + " uiScale=" + uiScaleX + "x" + uiScaleY
+                    + " clipFull=" + lastClipFull
+                    + " scaleEq=" + scaleOk
+                    + " delta=" + panDX + ',' + panDY
+                    + " viewID=" + getViewID());
+        }
 
         if (null == offscreenBuffer || offscreenBuffer.getWidth() != width || offscreenBuffer.getHeight() != height) {
             offscreenBuffer = getAcceleratedImage(this, width, height);
         }
 
-        if (!canUseBuffer || nonChangedLayersBuffer == null) {
+        if (!canReusePanned && (!canUseBuffer || nonChangedLayersBuffer == null)) {
             if (null == nonChangedLayersBuffer
                     || nonChangedLayersBuffer.getWidth() != width || nonChangedLayersBuffer.getHeight() != height) {
                 nonChangedLayersBuffer = getAcceleratedImage(this, width, height);
@@ -575,32 +631,72 @@ LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener {
             for (int i = 0; i < nonChangedLayersCount; i++) {
                 paintLayer(visibleLayers.get(i), g2);
             }
-        } else {
-            // Maybe there were more unchanged layers then last time - draw them to buffer
-            if (nonChangedLayers.size() != nonChangedLayersCount) {
-                Graphics2D g2 = nonChangedLayersBuffer.createGraphics();
-                g2.setClip(scaledClip);
-                g2.setTransform(trDef);
-                for (int i = nonChangedLayers.size(); i < nonChangedLayersCount; i++) {
-                    paintLayer(visibleLayers.get(i), g2);
-                }
-            }
         }
 
-        nonChangedLayers.clear();
-        if (nonChangedLayersCount > 0)
-            nonChangedLayers.addAll(visibleLayers.subList(0, nonChangedLayersCount));
         lastViewID = getViewID();
         lastClipBounds = g.getClipBounds();
+        lastPaintedScale = getScale();
+        lastPaintedCenter = getCenter();
+        lastPaintedLayers = new ArrayList<>(visibleLayers);
 
         Graphics2D tempG = offscreenBuffer.createGraphics();
         tempG.setClip(scaledClip);
         tempG.setTransform(new AffineTransform());
-        tempG.drawImage(nonChangedLayersBuffer, 0, 0, null);
+        if (canReusePanned) {
+            // Shift the previous composite by the pan delta and repaint the exposed strips.
+            // Strips overlap the blitted area by 1 px to hide seams; the horizontal band
+            // excludes the vertical strip's columns so the corner is painted only once.
+            int blitDX = (int) Math.round(panDX * uiScaleX);
+            int blitDY = (int) Math.round(panDY * uiScaleY);
+            tempG.setClip(0, 0, width, height);
+            tempG.drawImage(nonChangedLayersBuffer, blitDX, blitDY, null);
+            if (blitDX != 0) {
+                paintPanStrip(tempG, blitDX > 0 ? 0 : width + blitDX - 1, 0,
+                        blitDX > 0 ? blitDX + 1 : 1 - blitDX, height, visibleLayers, trDef);
+            }
+            if (blitDY != 0) {
+                paintPanStrip(tempG, blitDX == 0 ? 0 : blitDX > 0 ? blitDX + 1 : 0,
+                        blitDY > 0 ? 0 : height + blitDY - 1,
+                        blitDX == 0 ? width : blitDX > 0 ? width - blitDX - 1 : width + blitDX - 1,
+                        blitDY > 0 ? blitDY + 1 : 1 - blitDY, visibleLayers, trDef);
+            }
+            tempG.setClip(scaledClip);
+        } else {
+            tempG.drawImage(nonChangedLayersBuffer, 0, 0, null);
+        }
         tempG.setTransform(trDef);
 
-        for (int i = nonChangedLayersCount; i < visibleLayers.size(); i++) {
-            paintLayer(visibleLayers.get(i), tempG);
+        // Layers invalidated since the last frame (e.g. freshly loaded imagery tiles) are
+        // not repainted in full during a pan: a full repaint over the shifted composite
+        // would cover the content of the layers above them. They are painted strip-only
+        // and the remaining changes appear once the pan stops (clean rebuild).
+        if (!canReusePanned) {
+            for (int i = nonChangedLayersCount; i < visibleLayers.size(); i++) {
+                paintLayer(visibleLayers.get(i), tempG);
+            }
+        }
+        // Keep the buffer a complete composite of all layers so the next pan can shift it.
+        // Only when the whole viewport was painted: a partial repaint leaves stale pixels.
+        if (g.getClipBounds().contains(0, 0, width, height)) {
+            Graphics2D g2 = nonChangedLayersBuffer.createGraphics();
+            g2.drawImage(offscreenBuffer, 0, 0, null);
+            g2.dispose();
+        }
+        // Layers invalidated during the pan were only repainted inside the strip. Keep them
+        // marked so the first frame after the pan (with the view still) repaints them in
+        // full, showing the changes that were deferred.
+        if (canReusePanned) {
+            for (MapViewPaintable paintable : invalidated) {
+                invalidatedListener.invalidate(paintable);
+            }
+        }
+
+        // Imagery attribution is fixed to the viewport; painting it into the reused
+        // composite would leave copies behind while panning
+        for (Layer layer : visibleLayers) {
+            if (layer instanceof AbstractTileSourceLayer) {
+                ((AbstractTileSourceLayer<?>) layer).paintAttribution(tempG, this);
+            }
         }
 
         try {
@@ -663,6 +759,23 @@ LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener {
         }
     }
 
+    /**
+     * Paints the layers into one of the strips exposed by a pan. The strip rectangle is
+     * in device pixels; the layers paint in logical coordinates, so the clip is set under
+     * the identity transform and {@code trDef} is applied for the painting. The strip is
+     * cleared first because layers may paint with transparency over the old content.
+     */
+    private void paintPanStrip(Graphics2D g, int x, int y, int w, int h, List<Layer> layers, AffineTransform trDef) {
+        g.setTransform(new AffineTransform());
+        g.setClip(new Rectangle(x, y, w, h));
+        g.setColor(PaintColors.getBackgroundColor());
+        g.fillRect(x, y, w, h);
+        g.setTransform(trDef);
+        for (Layer layer : layers) {
+            paintLayer(layer, g);
+        }
+    }
+
     private void drawTemporaryLayers(Graphics2D tempG, Bounds box) {
         synchronized (temporaryLayers) {
             for (MapViewPaintable mvp : temporaryLayers) {
@@ -823,7 +936,6 @@ LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener {
         if (mapMover != null) {
             mapMover.destroy();
         }
-        nonChangedLayers.clear();
         synchronized (temporaryLayers) {
             temporaryLayers.clear();
         }
diff --git a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
index 510fad3..b0edb59 100644
--- a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
+++ b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
@@ -13,6 +13,7 @@ import java.awt.Graphics2D;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.Image;
+import java.awt.Rectangle;
 import java.awt.Shape;
 import java.awt.Toolkit;
 import java.awt.event.ActionEvent;
@@ -1564,6 +1565,26 @@ implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi
         // old and unused.
     }
 
+    /**
+     * Paints the imagery attribution for the current view. The map view calls this on its
+     * per-frame overlay: the attribution is fixed to the viewport, so painting it into the
+     * reused composite would leave copies behind while panning.
+     * @param g the overlay graphics
+     * @param mv the map view
+     */
+    public void paintAttribution(Graphics2D g, MapView mv) {
+        if (!isVisible()) {
+            return;
+        }
+        ProjectionBounds pb = mv.getState().getViewArea(new Rectangle(0, 0, mv.getWidth(), mv.getHeight()))
+                .getProjectionBounds();
+        EastNorth min = pb.getMin();
+        EastNorth max = pb.getMax();
+        int zoom = getDisplaySettings().isAutoZoom() ? getBestZoom() : currentZoomLevel;
+        attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(min), getShiftedCoord(max),
+                zoom, this);
+    }
+
     private void drawInViewArea(Graphics2D g, MapView mv, ProjectionBounds pb) {
         int zoom = currentZoomLevel;
         if (getDisplaySettings().isAutoZoom()) {
@@ -1662,11 +1683,6 @@ implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi
             this.paintTileText(t, g);
         }
 
-        EastNorth min = pb.getMin();
-        EastNorth max = pb.getMax();
-        attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(min), getShiftedCoord(max),
-                displayZoomLevel, this);
-
         g.setColor(Color.lightGray);
 
         if (ts.tooLarge()) {
diff --git a/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java b/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
index faaaf50..243ee31 100644
--- a/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
+++ b/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
@@ -1507,12 +1507,8 @@ public class OsmDataLayer extends AbstractOsmDataLayer
 
     @Override
     public void primitiveHovered(PrimitiveHoverEvent e) {
-        List<IPrimitive> primitives = new ArrayList<>(2);
-        primitives.add(e.getHoveredPrimitive());
-        primitives.add(e.getPreviousPrimitive());
-        primitives.removeIf(Objects::isNull);
-        resetTiles(primitives);
-        this.invalidate();
+        // The hover event does not change the rendered content, so nothing is invalidated
+        // here; listeners such as the PropertiesDialog use it to update their sidebar.
     }
 
     @Override
