From 73d70b92a92c1a829b227a2e85eedc5bfb80bb4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Wed, 23 Sep 2026 15:03:09 +0200 Subject: [PATCH] Cache the column list per table and requester `columns.for(table)` walked the plugin chain and returned a new array on every call, so reading it once per rendered row cost O(rows x columns) per render. Nothing at the call site said so, and a row component that takes the table and reads the list itself has the same cost without looking like it. `createCache` participates in autotracking, so the list is recomputed when the columns, their order or their visibility change, and reused otherwise. Cowritten by Claude --- table/src/plugins/-private/base.ts | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/table/src/plugins/-private/base.ts b/table/src/plugins/-private/base.ts index e0dec7a..7e78325 100644 --- a/table/src/plugins/-private/base.ts +++ b/table/src/plugins/-private/base.ts @@ -1,4 +1,5 @@ import { assert } from '@ember/debug'; +import { createCache, getValue } from '@glimmer/tracking/primitives/cache'; import { COLUMN_META_KEY, @@ -309,6 +310,19 @@ export const preferences = { * This works recursively up the plugin tree up until a plugin has no requirements, and then * all columns from the table are returned. */ +/** + * One cache per table and requester. `createCache` participates in + * autotracking, so the list is recomputed when the columns, their order or + * their visibility change, and reused otherwise. + * + * Without it, reading the list once per rendered row costs O(rows x columns) + * per render, and nothing at the call site says so. + */ +const COLUMNS_CACHE = new WeakMap< + object, + Map>> +>(); + function columnsFor< DataType = any, ColumnMeta = unknown, @@ -317,6 +331,33 @@ function columnsFor< >( table: Table, requester?: Plugin, +): Column[] { + let byRequester = COLUMNS_CACHE.get(table); + + if (byRequester === undefined) { + byRequester = new Map(); + COLUMNS_CACHE.set(table, byRequester); + } + + const key: unknown = requester ?? COLUMNS_CACHE; + let cache = byRequester.get(key); + + if (cache === undefined) { + cache = createCache(() => uncachedColumnsFor(table, requester)); + byRequester.set(key, cache); + } + + return getValue(cache) as Column[]; +} + +function uncachedColumnsFor< + DataType = any, + ColumnMeta = unknown, + Meta = unknown, + CellArgs = unknown, +>( + table: Table, + requester?: Plugin, ): Column[] { // Plugins hold columns of this same table, so they have its meta. return resolveColumns(table, requester) as Column<