From 3a08390a27942b1cdf43413420d7f98e1ca68aa4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 03:58:59 +0000 Subject: [PATCH] Perf: Index findViewById to avoid O(n) tree walk per lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 7 of the perf series. CPU profile on a 200-element page showed findViewRecursive at 509ms self-time, called ~420 times during boot via findViewById (316) and findContainerById (104). Each call was an O(n) walk of the full Marionette view tree. This builds a Map index lazily on first findViewById call within a frame, clears it via requestAnimationFrame, and validates _isDestroyed before returning a cached view. Cache misses fall through to a rebuild-and-retry so mid-frame view creation/destruction is handled safely. For the boot scenario (~420 lookups across ~200 elements in a tight burst), this turns 420 * 200 = 84,000 ops into ~200 (build) + 420 (lookups) ≈ 620 ops. Gated by e_memoize_active_controls. --- assets/dev/js/editor/document/component.js | 103 +++++++++++++++++---- 1 file changed, 85 insertions(+), 18 deletions(-) diff --git a/assets/dev/js/editor/document/component.js b/assets/dev/js/editor/document/component.js index 26420813515e..4294ce60bbb4 100644 --- a/assets/dev/js/editor/document/component.js +++ b/assets/dev/js/editor/document/component.js @@ -32,40 +32,107 @@ export default class Component extends ComponentBase { } defaultUtils() { - return { - findViewRecursive: ( parent, key, value, multiple = true ) => { - let found = []; - for ( const x in parent._views ) { - const view = parent._views[ x ]; + const findViewRecursive = ( parent, key, value, multiple = true ) => { + let found = []; + for ( const x in parent._views ) { + const view = parent._views[ x ]; + + if ( value === view.model.get( key ) ) { + found.push( view ); + if ( ! multiple ) { + return found; + } + } - if ( value === view.model.get( key ) ) { - found.push( view ); + if ( view.children ) { + const views = findViewRecursive( view.children, key, value, multiple ); + if ( views.length ) { + found = found.concat( views ); if ( ! multiple ) { return found; } } + } + } + return found; + }; + + // Per-frame index for findViewById. Without this, each lookup is an O(n) tree + // walk; with hundreds of lookups during boot on a heavy page (200+ elements), + // the cumulative cost reached ~500ms self-time in findViewRecursive. + // Gated by e_memoize_active_controls. + let viewIndex = null; + let viewIndexScheduled = false; + + const buildViewIndex = () => { + const index = new Map(); + const walk = ( children ) => { + if ( ! children?._views ) { + return; + } + for ( const k in children._views ) { + const view = children._views[ k ]; + const id = view.model?.get?.( 'id' ); + if ( id ) { + index.set( id, view ); + } if ( view.children ) { - const views = this.utils.findViewRecursive( view.children, key, value, multiple ); - if ( views.length ) { - found = found.concat( views ); - if ( ! multiple ) { - return found; - } - } + walk( view.children ); } } + }; + const root = elementor.getPreviewView?.()?.children; + if ( root ) { + walk( root ); + } + return index; + }; - return found; - }, + const scheduleIndexClear = () => { + if ( viewIndexScheduled ) { + return; + } + viewIndexScheduled = true; + const clear = () => { + viewIndex = null; + viewIndexScheduled = false; + }; + if ( 'function' === typeof window.requestAnimationFrame ) { + window.requestAnimationFrame( clear ); + } else { + setTimeout( clear, 16 ); + } + }; + + const findViewByIdIndexed = ( id ) => { + if ( ! viewIndex ) { + viewIndex = buildViewIndex(); + scheduleIndexClear(); + } + let view = viewIndex.get( id ); + if ( view && ! view._isDestroyed ) { + return view; + } + // Cache miss or stale: rebuild once and retry. Cheap when tree is unchanged, + // safe when it has churned. Falls back to false on genuine miss. + viewIndex = buildViewIndex(); + view = viewIndex.get( id ); + return ( view && ! view._isDestroyed ) ? view : false; + }; + + return { + findViewRecursive, findViewById: ( id ) => { - const elements = this.utils.findViewRecursive( + if ( elementorCommon?.config?.experimentalFeatures?.e_memoize_active_controls ) { + return findViewByIdIndexed( id ) || false; + } + const elements = findViewRecursive( elementor.getPreviewView().children, 'id', id, false, ); - return elements ? elements[ 0 ] : false; }, findContainerById: ( id ) => {