Broken debounce in tableheader.js causes layout thrashing on large tables (e.g. permissions page filtering)
>>> [!note] Migrated issue <!-- Drupal.org comment --> <!-- Migrated from issue #3595195. --> Reported by: [rashid yaqoob](https://www.drupal.org/user/3651721) Related to !797 >>> <h3>Problem/Motivation</h3> <p>The <code>ResizeObserver</code> callback in <code>js/tableheader.js</code> misuses <code>Drupal.debounce</code>:</p> <pre>const resizeHandler = new ResizeObserver(() =&gt; {<br>&nbsp; Drupal.debounce(this.updateTableHeader(el), 150);<br>});</pre><p><code>this.updateTableHeader(el)</code> is executed immediately, and its return value (<code>undefined</code>) is passed to <code>Drupal.debounce()</code>. As a result, a debounced function is created but never invoked. The intended 150 ms debounce therefore never occurs, and <code>updateTableHeader()</code> runs synchronously on every <code>ResizeObserver</code> notification.</p> <p>This has significant performance implications because <code>updateTableHeader()</code> performs multiple layout-sensitive operations. It calls <code>getBoundingClientRect()</code> on the <code>&amp;lt;thead&amp;gt;</code> and on each <code>&amp;lt;th&amp;gt;</code> element in both the original table and the sticky header clone, then writes calculated widths back as inline styles. These reads and writes force synchronous layout recalculations.</p> <p>On large tables that resize frequently, this can lead to severe UI jank. A notable example is the permissions page (<code>/admin/people/permissions</code>). Core's permission filter (<code>user.permissions.js</code>) continuously shows and hides table rows as the user types in the filter input. Each row visibility change can trigger a table resize, causing the <code>ResizeObserver</code> to fire and <code>updateTableHeader()</code> to perform a complete re-measurement of all header cells.</p> <p>In environments with many modules and roles, this can result in hundreds of forced reflows per keystroke, leading to long main-thread tasks and visibly degraded input responsiveness.</p> <h3>Steps to reproduce</h3> <ol> <li>Install a Drupal site with a large number of enabled modules and several user roles, using Gin as the administration theme.</li> <li>Navigate to <code>/admin/people/permissions</code>.</li> <li>Open browser DevTools and start a performance recording.</li> <li>Type into the <strong>"Filter by permission name"</strong> field.</li> <li>Observe long tasks in the performance profile dominated by <code>updateTableHeader()</code> and forced reflows.</li> <li>Notice that typing becomes sluggish, with the filter input appearing janky or temporarily unresponsive.</li> </ol> <h3>Proposed resolution</h3> <p>Create the debounced function once and invoke it from the observer callback:</p> <pre>const debouncedUpdate = Drupal.debounce(() =&gt; this.updateTableHeader(el), 150);<br>const resizeHandler = new ResizeObserver(() =&gt; {<br>&nbsp; debouncedUpdate();<br>});<br>resizeHandler.observe(el);</pre><p>This restores the intended behavior: at most one header re-measurement per 150 ms burst of resize events.</p> <h3>Remaining tasks</h3> <p>Review and commit.</p> <h3>User interface changes</h3> <p>None. Visual behavior is unchanged; the sticky header still updates after resize events.</p> <h3>API changes</h3> <p>None.</p> > Related issue: [Issue #3309353](https://www.drupal.org/node/3309353) > Related issue: [Issue #3472217](https://www.drupal.org/node/3472217)
issue