From 8ca92438e04c87fb3c9ca6dc4ba209046e66b212 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 00:39:36 +0000 Subject: [PATCH] =?UTF-8?q?Replace=20O(n=C2=B2)=20array=20scans=20in=20app?= =?UTF-8?q?Diff=20with=20Map/Set=20lookups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit appDiff runs on every app reload during `shopify app dev`, so it executes on each file change. It scanned the opposite extension array once per extension, making it O(n²) in the number of extensions. Index the old extensions by uid in a Map and the new uids in a Set so created/deleted/updated detection is O(n) with O(1) lookups. Behaviour is unchanged. Co-Authored-By: Claude Opus 4.8 --- .../src/cli/services/dev/app-events/app-diffing.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/app/src/cli/services/dev/app-events/app-diffing.ts b/packages/app/src/cli/services/dev/app-events/app-diffing.ts index 2f3bbe7b352..ba68d9b5b41 100644 --- a/packages/app/src/cli/services/dev/app-events/app-diffing.ts +++ b/packages/app/src/cli/services/dev/app-events/app-diffing.ts @@ -20,17 +20,20 @@ interface AppExtensionsDiff { */ export function appDiff(app: AppInterface, newApp: AppInterface, includeUpdated = true): AppExtensionsDiff { const oldExtensions = app.realExtensions - const oldExtensionsUids = oldExtensions.map((ext) => ext.uid) const newExtensions = newApp.realExtensions - const newExtensionsUids = newExtensions.map((ext) => ext.uid) - const createdExtensions = newExtensions.filter((ext) => !oldExtensionsUids.includes(ext.uid)) - const deletedExtensions = oldExtensions.filter((ext) => !newExtensionsUids.includes(ext.uid)) + // Indexing by uid keeps every lookup below O(1). The previous implementation scanned the opposite + // array for each extension, which is O(n²) on a path that runs on every file change during `app dev`. + const oldExtensionsByUid = new Map(oldExtensions.map((ext) => [ext.uid, ext])) + const newExtensionsUids = new Set(newExtensions.map((ext) => ext.uid)) + + const createdExtensions = newExtensions.filter((ext) => !oldExtensionsByUid.has(ext.uid)) + const deletedExtensions = oldExtensions.filter((ext) => !newExtensionsUids.has(ext.uid)) let updatedExtensions if (includeUpdated) { updatedExtensions = newExtensions.filter((ext) => { - const oldExtension = oldExtensions.find((oldExt) => oldExt.uid === ext.uid) + const oldExtension = oldExtensionsByUid.get(ext.uid) if (!oldExtension) return false const configChanged = JSON.stringify(oldExtension.configuration) !== JSON.stringify(ext.configuration) const extensionPathChanged = oldExtension.configurationPath !== ext.configurationPath