Skip to content

perf(core-flows): index variants by id in cart line item preparation - #16233

Merged
kodiakhq[bot] merged 2 commits into
medusajs:developfrom
irontaek:perf/cart-variant-lookup
Aug 4, 2026
Merged

kodiakhq[bot] merged 2 commits into
medusajs:developfrom
irontaek:perf/cart-variant-lookup

Conversation

@irontaek

Copy link
Copy Markdown
Contributor

Summary

What — prepareVariantsAndItemsWithPricesStep maps over the cart's line items and scans variantsData with .find() for every one of them:

const items = (inputItems ?? cart.items ?? []).map((item) => {
  ...
  const variant = variantsData.find((v) => v.id === item.variant_id)

variantsData is fetched for the variants those same items reference, so both sides grow together — the step is O(items × variants).

Why — this step is on the cart's hot path, not a one-off. It runs from:

caller when
cart/workflows/refresh-cart-items.ts cart refresh — essentially every cart mutation
cart/workflows/add-to-cart.ts add to cart
cart/workflows/create-carts.ts cart creation
order/workflows/create-order.ts checkout
order/workflows/add-line-items.ts order edits

How — index the variants by id once, then look up in O(1):

const variantsById = new Map<string, any>()
for (const variant of variantsData) {
  if (!variantsById.has(variant.id)) {
    variantsById.set(variant.id, variant)
  }
}
// ...
const variant = variantsById.get(item.variant_id!)

First match wins, so the result is identical to find. This is the same indexing pattern already used elsewhere in the cart flows for id matching.

Benchmark

Both variants copied verbatim from this file, output compared for equality before timing, median of 7 runs, one distinct variant per line item (the normal case):

cart items current indexed speedup
5 0.0075 ms 0.0023 ms 3.2x
20 0.0102 ms 0.0067 ms 1.5x
100 0.152 ms 0.028 ms 5.5x
500 2.241 ms 0.155 ms 14.4x
1,000 7.135 ms 0.208 ms 34.3x
2,000 27.602 ms 0.473 ms 58.4x

The shape is what matters: doubling the line items roughly quadruples the current cost (7.1 ms → 27.6 ms from 1,000 to 2,000) while the indexed version stays linear.

Being straightforward about the scale: on a typical B2C cart of 5–20 lines this is microseconds and nobody will notice. It becomes real on large carts — bulk/B2B orders, imported carts, quote-style flows — where a single refresh can spend tens of milliseconds scanning. The change costs nothing at small sizes, so it is a floor-raiser rather than a fix for a reported incident.

Testing

Behaviour is unchanged: Map.get replaces a find over the same array with the same key, and the helper keeps first-occurrence-wins.

Honest about what I could and couldn't run locally:

  • Type safety of the change — variantsData is any[] here, so the Map<string, any> and .get(item.variant_id!) introduce no new narrowing. I checked the changed file in isolation and it produces no type errors beyond the module-resolution ones that the same file already produces without my change.
  • I could not run the monorepo typecheck or the integration suite locally (they need the full workspace build plus a Postgres instance), so CI is the first real run. Happy to iterate if anything goes red.
  • No dedicated unit test exists for this step today. If you'd like one added as part of this PR, say the word and I'll add coverage for the mapping (variant found / not found / unpublished product / missing price) rather than for the lookup mechanism itself.

Added a changeset (@medusajs/core-flows patch).

Closes #16232

prepareVariantsAndItemsWithPricesStep mapped over the cart's line items and
scanned variantsData with .find() for each one. variantsData is fetched for the
variants those same items reference, so both sides grow together and the step
was O(items x variants).

The step runs from refresh-cart-items (every cart mutation), add-to-cart,
create-carts, create-order and add-line-items, so the cost is paid repeatedly
across a cart's lifecycle.

Index by id once and look up in O(1). First match wins, so the result is
identical to find.

Measured (both variants copied verbatim, output compared for equality first,
median of 7 runs, one distinct variant per line item):
    100 items   0.152ms ->  0.028ms    5.5x
    500 items   2.241ms ->  0.155ms   14.4x
  1,000 items   7.135ms ->  0.208ms   34.3x
  2,000 items  27.602ms ->  0.473ms   58.4x

For a typical B2C cart of 5-20 lines this is microseconds; it matters on large
carts (bulk/B2B, imported carts).

Closes medusajs#16232
@irontaek
irontaek requested a review from a team as a code owner July 29, 2026 05:16
@changeset-bot

changeset-bot Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e3b01e9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 79 packages
Name Type
@medusajs/core-flows Patch
@medusajs/medusa Patch
@medusajs/test-utils Patch
integration-tests-http Patch
@medusajs/loyalty-plugin Patch
@medusajs/medusa-oas-cli Patch
@medusajs/analytics Patch
@medusajs/api-key Patch
@medusajs/auth Patch
@medusajs/caching Patch
@medusajs/cart Patch
@medusajs/currency Patch
@medusajs/customer Patch
@medusajs/file Patch
@medusajs/fulfillment Patch
@medusajs/index Patch
@medusajs/inventory Patch
@medusajs/link-modules Patch
@medusajs/locking Patch
@medusajs/notification Patch
@medusajs/order Patch
@medusajs/payment Patch
@medusajs/pricing Patch
@medusajs/product Patch
@medusajs/promotion Patch
@medusajs/rbac Patch
@medusajs/region Patch
@medusajs/sales-channel Patch
@medusajs/settings Patch
@medusajs/stock-location Patch
@medusajs/store Patch
@medusajs/tax Patch
@medusajs/translation Patch
@medusajs/user Patch
@medusajs/workflow-engine-inmemory Patch
@medusajs/workflow-engine-redis Patch
@medusajs/draft-order Patch
@medusajs/oas-github-ci Patch
@medusajs/cache-inmemory Patch
@medusajs/cache-redis Patch
@medusajs/event-bus-local Patch
@medusajs/event-bus-redis Patch
@medusajs/analytics-local Patch
@medusajs/analytics-posthog Patch
@medusajs/auth-emailpass Patch
@medusajs/auth-github Patch
@medusajs/auth-google Patch
@medusajs/caching-redis Patch
@medusajs/file-local Patch
@medusajs/file-s3 Patch
@medusajs/fulfillment-manual Patch
@medusajs/locking-postgres Patch
@medusajs/locking-redis Patch
@medusajs/notification-local Patch
@medusajs/notification-sendgrid Patch
@medusajs/payment-stripe Patch
@medusajs/framework Patch
@medusajs/js-sdk Patch
@medusajs/modules-sdk Patch
@medusajs/orchestration Patch
@medusajs/query Patch
@medusajs/types Patch
@medusajs/utils Patch
@medusajs/workflows-sdk Patch
@medusajs/http-types-generator Patch
@medusajs/cli Patch
@medusajs/deps Patch
@medusajs/eslint-plugin Patch
@medusajs/telemetry Patch
@medusajs/admin-bundler Patch
@medusajs/admin-sdk Patch
@medusajs/admin-shared Patch
@medusajs/admin-vite-plugin Patch
@medusajs/dashboard Patch
@medusajs/icons Patch
@medusajs/toolbox Patch
@medusajs/ui-preset Patch
create-medusa-app Patch
@medusajs/ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@medusa-os-bot

medusa-os-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Thanks for the contribution! A few items need to be addressed before this can move forward:

Performance optimization that replaces a linear scan with a Map-based O(1) lookup in prepareVariantsAndItemsWithPricesStep. The logic is correct and behavior-preserving. One required change: the changeset message uses an unsupported perf(...) prefix. No tests were added; for a trivial, behavior-identical optimization this is borderline acceptable, but the author has offered to add them if requested.

  • .changeset/cart-variant-lookup-index.md: changeset message uses perf(core-flows): which is not an allowed format. Change to chore(core-flows): per contribution guidelines (allowed: fix, feat, chore).

Triggered by: manual workflow dispatch

The changeset bot only accepts fix/feat/chore; perf(...) was rejected.
@medusa-os-bot

medusa-os-bot Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Thanks for the contribution! Initial automated review looks good.

Performance optimization replacing a linear find scan with a Map-based O(1) lookup in prepareVariantsAndItemsWithPricesStep. The previously required fix (changeset message format) has been addressed — the message now correctly uses the chore(core-flows): prefix. The code change is behavior-identical (first-match-wins semantics preserved via the !variantsById.has guard), no security issues, no bugs, and no regressions introduced.

Triggered by: new commit pushed

@shahednasser shahednasser left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Quadratic variant lookup in prepareVariantsAndItemsWithPricesStep (cart refresh path)

2 participants