From 38fec6e82a605d25ce3c151ae65d03680966b12f Mon Sep 17 00:00:00 2001 From: Navneet Date: Fri, 30 Jan 2026 16:10:12 +0530 Subject: [PATCH 1/6] Improve zoom/pan performance by caching tag classes and label text widths --- modules/svg/labels.js | 32 ++++++++++++++++++++++---------- modules/svg/tag_classes.js | 18 +++++++++++++++++- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/modules/svg/labels.js b/modules/svg/labels.js index 1d4c1f349eb..2b498c9b2ec 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -789,19 +789,31 @@ export function svgLabels(projection, context) { const _textWidthCache = {}; export function textWidth(text, size, container) { let c = _textWidthCache[size]; - if (!c) c = _textWidthCache[size] = {}; + if (!c) { + c = _textWidthCache[size] = { widths: {}, measurer: null }; + } + + if (c.widths[text]) { + return c.widths[text]; + } - if (c[text]) { - return c[text]; + // Avoid repeated create/remove of SVG text nodes on every zoom/pan. + // The measured width depends only on text + font size/style, which + // are stable during zoom/pan, so caching is safe. + let elem = c.measurer; + if (!elem) { + elem = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + elem.style.fontSize = `${size}px`; + elem.style.fontWeight = 'bold'; + elem.style.visibility = 'hidden'; + elem.style.pointerEvents = 'none'; + container.appendChild(elem); + c.measurer = elem; } - const elem = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - elem.style.fontSize = `${size}px`; - elem.style.fontWeight = 'bold'; + elem.textContent = text; - container.appendChild(elem); - c[text] = elem.getComputedTextLength(); - elem.remove(); - return c[text]; + c.widths[text] = elem.getComputedTextLength(); + return c.widths[text]; } diff --git a/modules/svg/tag_classes.js b/modules/svg/tag_classes.js index b7e2c9358fd..7d18a26b121 100644 --- a/modules/svg/tag_classes.js +++ b/modules/svg/tag_classes.js @@ -17,6 +17,10 @@ export function svgTagClasses() { 'man_made', 'indoor', 'construction', 'proposed' ]; var _tags = function(entity) { return entity.tags; }; + // Cache computed class strings by tag object identity + base class. + // Safe for zoom/pan because tags do not change, and if tags change + // the graph creates new tag objects (cache miss => recompute). + var _classCache = new WeakMap(); var tagClasses = function(selection) { @@ -29,7 +33,19 @@ export function svgTagClasses() { var t = _tags(entity); - var computed = tagClasses.getClassesString(t, value); + // Avoid recomputing tagClasses for unchanged tags during zoom/pan. + // Cache is keyed by tag object identity and base class string. + var byBase = _classCache.get(t); + if (!byBase) { + byBase = new Map(); + _classCache.set(t, byBase); + } + + var computed = byBase.get(value); + if (!computed) { + computed = tagClasses.getClassesString(t, value); + byBase.set(value, computed); + } if (computed !== value) { d3_select(this).attr('class', computed); From 202570bd97885b36b9667f7418995d535d7b4de3 Mon Sep 17 00:00:00 2001 From: Navneet Date: Sat, 31 Jan 2026 19:53:05 +0530 Subject: [PATCH 2/6] perf: reduce tagClasses cache misses during zoom redraws --- modules/svg/tag_classes.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/modules/svg/tag_classes.js b/modules/svg/tag_classes.js index 7d18a26b121..5516c865d5e 100644 --- a/modules/svg/tag_classes.js +++ b/modules/svg/tag_classes.js @@ -34,18 +34,7 @@ export function svgTagClasses() { var t = _tags(entity); // Avoid recomputing tagClasses for unchanged tags during zoom/pan. - // Cache is keyed by tag object identity and base class string. - var byBase = _classCache.get(t); - if (!byBase) { - byBase = new Map(); - _classCache.set(t, byBase); - } - - var computed = byBase.get(value); - if (!computed) { - computed = tagClasses.getClassesString(t, value); - byBase.set(value, computed); - } + var computed = tagClasses.getClassesString(t, value); if (computed !== value) { d3_select(this).attr('class', computed); @@ -55,6 +44,16 @@ export function svgTagClasses() { tagClasses.getClassesString = function(t, value) { + // Fast path: return cached class string for identical tags + base class. + var byBase = _classCache.get(t); + if (!byBase) { + byBase = new Map(); + _classCache.set(t, byBase); + } + + var cached = byBase.get(value); + if (cached) return cached; + var primary, status; var i, j, k, v; @@ -177,10 +176,13 @@ export function svgTagClasses() { // ensure that classes for tags keys/values with special characters like spaces // are not added to the DOM, because it can cause bizarre issues (#9448) - return classes + var computed = classes .filter(klass => /^[-_a-z0-9]+$/.test(klass)) .join(' ') .trim(); + + byBase.set(value, computed); + return computed; }; From 26df5e63dcf88d15bcc3d9f0c82a29b84c1bdfaa Mon Sep 17 00:00:00 2001 From: Navneet Date: Thu, 12 Feb 2026 15:30:35 +0530 Subject: [PATCH 3/6] perf: cache tag-derived classes with WeakMap --- modules/svg/tag_classes.js | 38 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/modules/svg/tag_classes.js b/modules/svg/tag_classes.js index 5516c865d5e..15f19356807 100644 --- a/modules/svg/tag_classes.js +++ b/modules/svg/tag_classes.js @@ -1,7 +1,7 @@ import { select as d3_select } from 'd3-selection'; import { osmPathHighwayTagValues, osmPavedTags, osmSemipavedTags, osmLifecyclePrefixes } from '../osm/tags'; - +var _classCache = new WeakMap(); export function svgTagClasses() { var primaries = [ 'building', 'highway', 'railway', 'waterway', 'aeroway', 'aerialway', @@ -17,10 +17,7 @@ export function svgTagClasses() { 'man_made', 'indoor', 'construction', 'proposed' ]; var _tags = function(entity) { return entity.tags; }; - // Cache computed class strings by tag object identity + base class. - // Safe for zoom/pan because tags do not change, and if tags change - // the graph creates new tag objects (cache miss => recompute). - var _classCache = new WeakMap(); + // Cache tag-derived classes by tag object identity. var tagClasses = function(selection) { @@ -33,7 +30,6 @@ export function svgTagClasses() { var t = _tags(entity); - // Avoid recomputing tagClasses for unchanged tags during zoom/pan. var computed = tagClasses.getClassesString(t, value); if (computed !== value) { @@ -44,16 +40,6 @@ export function svgTagClasses() { tagClasses.getClassesString = function(t, value) { - // Fast path: return cached class string for identical tags + base class. - var byBase = _classCache.get(t); - if (!byBase) { - byBase = new Map(); - _classCache.set(t, byBase); - } - - var cached = byBase.get(value); - if (cached) return cached; - var primary, status; var i, j, k, v; @@ -66,7 +52,7 @@ export function svgTagClasses() { } // preserve base classes (nothing with `tag-`) - var classes = value.trim().split(/\s+/) + var baseClasses = value.trim().split(/\s+/) .filter(function(klass) { return klass.length && !/^tag-/.test(klass); }) @@ -74,6 +60,14 @@ export function svgTagClasses() { return (klass === 'line' || klass === 'area') ? (overrideGeometry || klass) : klass; }); + // Fast path: return cached tag-derived classes if available + var tagDerivedClasses = _classCache.get(t); + if (tagDerivedClasses) { + return baseClasses.concat(tagDerivedClasses).join(' ').trim(); + } + + var classes = []; + // pick at most one primary classification tag.. for (i = 0; i < primaries.length; i++) { k = primaries[i]; @@ -176,13 +170,9 @@ export function svgTagClasses() { // ensure that classes for tags keys/values with special characters like spaces // are not added to the DOM, because it can cause bizarre issues (#9448) - var computed = classes - .filter(klass => /^[-_a-z0-9]+$/.test(klass)) - .join(' ') - .trim(); - - byBase.set(value, computed); - return computed; + var tagDerived = classes.filter(klass => /^[-_a-z0-9]+$/.test(klass)); + _classCache.set(t, tagDerived); + return baseClasses.concat(tagDerived).join(' ').trim(); }; From dc77c8270aa0b1307830707eaaa69543f2b47fb0 Mon Sep 17 00:00:00 2001 From: Navneet Date: Fri, 13 Feb 2026 16:13:14 +0530 Subject: [PATCH 4/6] changelog: add entry for #11834 performance improvement git rebase --continue --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8a8ee79bc9..054d8a5e7a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,7 +58,11 @@ _Breaking developer changes, which may affect downstream projects or sites that * When viewing the help information for tags or presets, use locale-specific properties if available ([#11760], thanks [@k-yle]) * Show country names in your preferred language and country flag emoji in the Country field dropdown ([#11783], thanks [@Razen04]) #### :hourglass: Performance + * iD is now twice as fast during long editing sessions ([#11861], thanks [@k-yle]) + +* Improve zoom/pan performance by avoiding redundant tag class recomputation ([#11834], thanks [@1-navneet]) + #### :mortar_board: Walkthrough / Help #### :rocket: Presets * Add dedicated styling for `highway=ladder` to make it distinguishable from `highway=steps` ([#11799], thanks [@bhavyaKhatri2703]) @@ -82,20 +86,29 @@ _Breaking developer changes, which may affect downstream projects or sites that [#11784]: https://github.com/openstreetmap/iD/pull/11784 [#11794]: https://github.com/openstreetmap/iD/pull/11794 [#11799]: https://github.com/openstreetmap/iD/issues/11799 + [#11783]: https://github.com/openstreetmap/iD/pull/11783 [#11861]: https://github.com/openstreetmap/iD/pull/11861 [#11862]: https://github.com/openstreetmap/iD/pull/11862 + [#11870]: https://github.com/openstreetmap/iD/issues/11870 [#11904]: https://github.com/openstreetmap/iD/issues/11904 + +[#11834]: https://github.com/openstreetmap/iD/pull/11834 + [@ilias52730]: https://github.com/ilias52730 [@Razen04]: https://github.com/Razen04 [@homersimpsons]: https://github.com/homersimpsons [@omsaraykar]: https://github.com/omsaraykar [@Kaushik4141]: https://github.com/Kaushik4141 + [@Kayd-06]: https://github.com/Kayd-06 [@JaiswalShivang]: https://github.com/JaiswalShivang +[@1-navneet]: https://github.com/1-navneet + + # v2.37.3 ##### 2025-10-31 From ecae0800f7931c7ccde2450df124e60c2237376c Mon Sep 17 00:00:00 2001 From: Navneet Date: Mon, 23 Feb 2026 13:21:22 +0530 Subject: [PATCH 5/6] Address review feedback --- CHANGELOG.md | 1 - modules/svg/tag_classes.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 054d8a5e7a0..e443b2ebcdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,7 +86,6 @@ _Breaking developer changes, which may affect downstream projects or sites that [#11784]: https://github.com/openstreetmap/iD/pull/11784 [#11794]: https://github.com/openstreetmap/iD/pull/11794 [#11799]: https://github.com/openstreetmap/iD/issues/11799 - [#11783]: https://github.com/openstreetmap/iD/pull/11783 [#11861]: https://github.com/openstreetmap/iD/pull/11861 [#11862]: https://github.com/openstreetmap/iD/pull/11862 diff --git a/modules/svg/tag_classes.js b/modules/svg/tag_classes.js index 15f19356807..17beefca2a4 100644 --- a/modules/svg/tag_classes.js +++ b/modules/svg/tag_classes.js @@ -16,8 +16,8 @@ export function svgTagClasses() { 'public_transport', 'location', 'parking', 'golf', 'type', 'leisure', 'man_made', 'indoor', 'construction', 'proposed' ]; - var _tags = function(entity) { return entity.tags; }; // Cache tag-derived classes by tag object identity. + var _tags = function(entity) { return entity.tags; }; var tagClasses = function(selection) { From c2f8bbc1c3979b5e86d622d4969643d99e0b0986 Mon Sep 17 00:00:00 2001 From: Navneet Date: Mon, 23 Feb 2026 13:36:30 +0530 Subject: [PATCH 6/6] Fix changelog formatting --- CHANGELOG.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e443b2ebcdc..aae66e2c528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,11 +58,8 @@ _Breaking developer changes, which may affect downstream projects or sites that * When viewing the help information for tags or presets, use locale-specific properties if available ([#11760], thanks [@k-yle]) * Show country names in your preferred language and country flag emoji in the Country field dropdown ([#11783], thanks [@Razen04]) #### :hourglass: Performance - * iD is now twice as fast during long editing sessions ([#11861], thanks [@k-yle]) - * Improve zoom/pan performance by avoiding redundant tag class recomputation ([#11834], thanks [@1-navneet]) - #### :mortar_board: Walkthrough / Help #### :rocket: Presets * Add dedicated styling for `highway=ladder` to make it distinguishable from `highway=steps` ([#11799], thanks [@bhavyaKhatri2703]) @@ -89,22 +86,16 @@ _Breaking developer changes, which may affect downstream projects or sites that [#11783]: https://github.com/openstreetmap/iD/pull/11783 [#11861]: https://github.com/openstreetmap/iD/pull/11861 [#11862]: https://github.com/openstreetmap/iD/pull/11862 - [#11870]: https://github.com/openstreetmap/iD/issues/11870 [#11904]: https://github.com/openstreetmap/iD/issues/11904 - [#11834]: https://github.com/openstreetmap/iD/pull/11834 - [@ilias52730]: https://github.com/ilias52730 [@Razen04]: https://github.com/Razen04 [@homersimpsons]: https://github.com/homersimpsons [@omsaraykar]: https://github.com/omsaraykar [@Kaushik4141]: https://github.com/Kaushik4141 - [@Kayd-06]: https://github.com/Kayd-06 [@JaiswalShivang]: https://github.com/JaiswalShivang - - [@1-navneet]: https://github.com/1-navneet