Skip to content

perf: improve lingo performance — personalization, geo, lingo, XLG#6210

Open
mokimo wants to merge 13 commits into
stagefrom
mokimo-perf-combined
Open

perf: improve lingo performance — personalization, geo, lingo, XLG#6210
mokimo wants to merge 13 commits into
stagefrom
mokimo-perf-combined

Conversation

@mokimo

@mokimo mokimo commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Before

Multiple sequential render blocking calls
image

After

Parallelize and prefetch needed code
image

Description

Combined quick-win performance improvements targeting LCP on Lingo-active pages (e.g. /fr/creativecloud). Rolls up and supersedes the four individual PRs listed below — they can be closed once this merges.

Supersedes:


1. Personalization module modulepreload + inline sanitizeHtml (#6206)

personalization.js → sanitizeHtml.js → promo-utils.js loaded serially (~130ms). Added modulepreload hints before the getCountry() await so all three start in parallel. Inlined sanitizeHtml.js (single-consumer static import) to eliminate that extra request entirely.

Gain: ~80ms off personalization apply time.


2. geo2 5s timeout + in-flight dedup (#6207)

geo2.adobe.com had no timeout (browser TCP timeout ~30s) and no dedup — concurrent callers each issued a separate fetch. resolveDetectedMarketCountry() compounded this with a serial retry on failure. Added AbortController with 5s timeout, cached the in-flight promise, and removed the redundant fallback call.

Gain: eliminates pathological 30–60s stall; no impact on typical fast-path.


3. Parallel personalization manifest fetches + getEntitlementMap dedup (#6208)

getManifestConfig iterated manifests serially; categorizeActions used a serial for-await loop. Both converted to Promise.all. When manifests run concurrently, each was calling getEntitlementMap independently — fixed with an in-flight promise cache on config.mep.entitlementMapFetch. Also moved loadLingoIndexes before decorateDocumentExtras to overlap lingo index fetches with header decoration, and deduped identical primary/base query-index URLs.

Gain: 300–800ms on pages with 3+ manifests.


4. Lingo Wave 2 defer + priority: 'low' (#6209)

lingo-site-mapping.json (0.9 KB) resolves in ~70ms and immediately fired 6 cross-site query-index fetches (Wave 2) while Wave 1's 45 KB + 26 KB primary/base files were still downloading. On 4G this bandwidth race delayed Wave 1 completion by ~220ms.

Three changes: fetchOptions param on processQueryIndexMap; priority: 'low' on the site-mapping fetch; await Wave 1 (Promise.all of primary + base) before firing Wave 2's forEach. Wave 2 calls also pass priority: 'low'.

Wave 1 unchanged — fires immediately at default priority for LCP-section link localization.

Gain: ~220ms bandwidth relief on 4G; Wave 2 no longer races Wave 1.


5. XLG preload URL alignment (new)

getXLGListURL returns https://www.adobe.com/federal/assets/data/mep-xlg-tags.json. The rel=preload link used that raw URL; fetchData passes it through normalizePath which resolves it via getFederatedUrl to the environment-specific federated root (e.g. main--federal--adobecom.aem.live on preview). Two different resolved URLs → browser cache miss → two round trips. Wrapping the preload argument in normalizePath() aligns both calls to the same URL in every environment.

Gain: eliminates one redundant XLG JSON fetch on non-prod environments.


Resolves: MWPW-TBD

Test URLs (da-cc fr/creativecloud):

🤖 Generated with Claude Code

mokimo and others added 6 commits June 22, 2026 16:43
personalization.js, sanitizeHtml.js (static import), and promo-utils.js
were loaded serially — each triggered only after the previous finished.
On adobe.com/fr/creativecloud this created a ~130ms serial chain starting
at t=499ms (personalization → +41ms sanitizeHtml → +44ms promo-utils).

Adding modulepreload hints for all three in checkForPageMods(), before the
getCountry() await, overlaps their fetches with country detection so the
browser has them in the module cache by the time await import() fires.
Verified locally: all three now start in parallel at the same timestamp.

Also deduplicates the getConfig().base call in the same block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…request

sanitizeHtml.js had a single consumer (personalization.js) and was always
loaded as a static import — meaning every page with personalization paid
for an extra serial module fetch. Moving the logic directly into
personalization.js eliminates that request entirely.

- Inline stringToHTML, isPossiblyDangerous, sanitizeHtmlNode, and
  sanitizeHtmlBody into personalization.js as private functions
  (sanitizeHtmlBody exported for testability)
- Delete libs/utils/sanitizeHtml.js
- Remove the now-unnecessary modulepreload hint for sanitizeHtml.js
- Update sanitizeHtml.test.js to import from personalization.js and
  drop the sanitizeHtml (firstChild) describe block — that export is gone

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… call

geo2.adobe.com/json/ was fetched with no timeout (browser TCP timeout ~30s)
and no deduplication — concurrent callers each started an independent fetch.
resolveDetectedMarketCountry() compounded this by making a second serial
geo2 attempt on failure, meaning a blocked geo2 could cost 60s total.

Changes:
- geo.js: wrap fetch in AbortController with 5s timeout; cache the
  in-flight promise so concurrent calls share one fetch instead of
  each issuing a duplicate request
- utils.js: remove the redundant getAkamaiCode() fallback block from
  resolveDetectedMarketCountry() — getCountry() already tried geo2
  and returned null, an immediate retry will have the same result

lana.js loading late is a symptom, not a cause: it lazy-loads on the
first window.lana.log() call, which fires when geo2 times out.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dexes earlier

- Parallelize getManifestConfig() calls with Promise.all (was serial for-loop) — 300-800ms gain on pages with multiple manifests
- Parallelize categorizeActions() calls with Promise.all (synchronous but eliminates microtask overhead)
- Add in-flight promise dedup to getEntitlementMap() so concurrent callers share one XLG tags fetch instead of each issuing their own
- Move section setup and loadLingoIndexes() before decorateDocumentExtras() so query-index fetches start while header/meta decoration runs (parallel instead of serial)
- Deduplicate processQueryIndexMap calls when primary and base locale URLs resolve to the same path (avoids double-fetch on prod with root locale)
- Add market.js modulepreload hint before the preview.js dynamic import so both modules load in parallel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Lingo-active pages (e.g. /fr/creativecloud), lingo-site-mapping.json
resolves quickly (~70ms) and immediately triggers 6 cross-site query index
fetches (Wave 2) while Wave 1's 45KB + 26KB files are still downloading.
On 4G this bandwidth race delays Wave 1 completion by ~220ms, pushing back
the LCP image fetch start.

Three changes:
- Add fetchOptions param to processQueryIndexMap so callers can control
  fetch priority independently.
- Fetch lingo-site-mapping.json with { priority: 'low' } — it has no LCP
  dependency and should not compete with hero CSS/JS.
- Await Wave 1 (primary + base pathsRequests) inside the lingoSiteMapping
  IIFE before firing the Wave 2 forEach, so cross-site indexes only start
  after the bandwidth-heavy Wave 1 files have landed. Wave 2 calls also
  pass { priority: 'low' }.

Wave 1 is unchanged — fires immediately at default priority for LCP section
link localization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…request

getXLGListURL() returns https://www.adobe.com/federal/assets/data/mep-xlg-tags.json.
fetchData() passes it through normalizePath() which resolves it via getFederatedUrl()
to the environment-specific federated root (e.g. main--federal--adobecom.aem.live
on preview). The rel=preload loadLink call was using the raw www.adobe.com URL,
so the browser preloaded one URL and fetched a different resolved URL — cache miss,
two round trips.

Wrapping the preload argument in normalizePath() makes both calls use the same
resolved URL in every environment, letting the browser reuse the preloaded response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mokimo mokimo requested a review from a team as a code owner June 22, 2026 14:45
@aem-code-sync

aem-code-sync Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Hello, I'm the AEM Code Sync Bot and I will run some actions to deploy your branch.
In case there are problems, just click the checkbox below to rerun the respective action.

  • Re-sync branch
Commits

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…arlier

saveToMmm() posts analytics to a Lambda; its result is unused, but the
await was blocking checkForPageMods() for the full Lambda round-trip
(~500ms visible in waterfall). Dropping the await unblocks section
processing and shifts fragment.js loading ~500ms earlier.

Add a modulepreload for fragment.js right after sections are built
(post-personalization DOM is final at that point), so the browser fetches
it in parallel with section decoration instead of cold-loading it when the
first fragment selector match triggers a dynamic import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@aem-code-sync aem-code-sync Bot temporarily deployed to mokimo-perf-combined June 22, 2026 15:05 Inactive
@mokimo mokimo changed the title perf: CC/FR LCP quick wins — personalization, geo, lingo, XLG perf: improve lingo performance — personalization, geo, lingo, XLG Jun 22, 2026
preview.js has heavy static deps (merch.js, caas/utils.js) that were
loading right after personalization completes — still inside the LCP
window. By deferring the import to the milo:deferred event (fired after
all sections are processed), the entire module graph stays out of the
LCP window with no user-visible impact.

Also removes the market.js modulepreload from the LCP window since
market.js is only needed as a static dep of preview.js.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@aem-code-sync aem-code-sync Bot temporarily deployed to mokimo-perf-combined June 22, 2026 16:39 Inactive
The milo:deferred approach caused a performance regression: preview.js has
market.js, mas-mep-utils.js, merch.js, and caas/utils.js as static imports.
Those modules were previously loading right after personalization (in parallel
with section processing), warming the module cache for the merch/MAS blocks.
Deferring to milo:deferred pushed the whole graph out past all sections,
making those modules cold and sequential when the merch block actually needs them.

Fire-and-forget is the right approach: removecing the await unblocks
checkForPageMods() from the Lambda round-trip, while still starting
preview.js + its static deps early enough to benefit the merch blocks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@aem-code-sync aem-code-sync Bot temporarily deployed to mokimo-perf-combined June 22, 2026 16:51 Inactive
… dep chain

hero-marquee.js awaits iconography.css on its critical path, blocking section 1
completion and delaying everything after it (including section 2 blocks like
commerce). Adding rel=preload for iconography.css and breakpoint-theme.css in
preloadBlockResources when hero-marquee is detected lets the browser fetch them
in parallel with the block JS, so the await resolves from cache.

preview.js -> caas/utils.js -> {lingo-active.js, getUuid.js} is a 2-level-deep
sequential discovery chain. Adding modulepreload hints for both alongside the
existing market.js hint lets the browser fetch them in parallel with the rest of
the preview.js module graph rather than waiting for caas/utils.js to parse first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@aem-code-sync aem-code-sync Bot temporarily deployed to mokimo-perf-combined June 22, 2026 17:01 Inactive
Comment thread libs/utils/sanitizeHtml.js
Comment thread libs/features/personalization/personalization.js
@markpadbe markpadbe requested a review from a team June 22, 2026 17:44
Comment thread libs/features/personalization/personalization.js
@markpadbe markpadbe requested a review from a team June 22, 2026 18:17
…ana logging

- Restore libs/utils/sanitizeHtml.js (both sanitizeHtml and sanitizeHtmlBody
  exports) so cc dynamic imports don't 404
- Revert inline copy in personalization.js; use static import instead
- Add modulepreload for sanitizeHtml.js in utils.js alongside personalization.js
  preload so both load in parallel before getCountry() await
- Add window.lana?.log to preview.js .catch so MEP save errors surface in monitoring
- Add comment on Promise.all/categorizeActions ordering invariant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@aem-code-sync aem-code-sync Bot temporarily deployed to mokimo-perf-combined June 23, 2026 11:35 Inactive
…eHtml tests back

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mokimo mokimo requested a review from markpadbe June 23, 2026 11:53
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

This PR has not been updated recently and will be closed in 7 days if no action is taken. Please ensure all checks are passing, https://github.com/orgs/adobecom/discussions/997 provides instructions. If the PR is ready to be merged, please mark it with the "Ready for Stage" label.

@github-actions github-actions Bot added the Stale label Jul 1, 2026

@vhargrave vhargrave left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

very nice! :)

@github-actions github-actions Bot removed the Stale label Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants