Skip to content

perf: combined performance enhancements (FORMS-24968/24969/24970/24971/24975)#207

Open
dmaurya929 wants to merge 15 commits into
mainfrom
perf/combined-performance-enhancements
Open

perf: combined performance enhancements (FORMS-24968/24969/24970/24971/24975)#207
dmaurya929 wants to merge 15 commits into
mainfrom
perf/combined-performance-enhancements

Conversation

@dmaurya929

@dmaurya929 dmaurya929 commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Five performance enhancements bundled into a single PR:

FORMS-24968 — Form-bundle rollup pipeline

  • Collapses the serial ES module hop chain (form.jsrules/index.js → workers + runtime) into a single pre-bundled file (form-bundle.min.js) via Rollup.
  • form.js and rules/index.js become thin re-export shims; original source preserved in form.source.js and rules/index.source.js.
  • scripts/swap-shims.js + .husky/pre-commit.mjs keep the bundle in sync on commit.

FORMS-24969 — Eager/lazy custom-functions split

  • New build:custom-functions CLI analyses which custom functions fire at load-time vs on user interaction (from live page fetch or local form JSON).
  • Produces an eager bundle (myfn.min.js) with real impls for load-time functions and async/sync stubs for the rest, plus a lazy bundle (myfn-lazy.min.js) with full implementations deferred to the 3s mark.
  • Entry file (myfn.js) becomes a transparent shim — form JSON unchanged.
  • functionRegistration.js modulepreloads the eager bundle and speculatively prefetches the lazy bundle; stores loadLazyBundle on window.hlx.
  • scripts.js triggers window.hlx.loadLazyBundle() at 3s — before typical first user interaction — so stubs are already backed by real impls.
  • rollup/functions.rollup.config.js builds the OOTB functions.min.js bundles referenced by eager bundles.

FORMS-24970 — Critical CSS split convention

  • loadFormCustomStyles probes for a <name>-critical.css companion file via HEAD request.
  • If found: critical CSS loads immediately; full stylesheet deferred to window.load (post-LCP).
  • If not found: full CSS loads immediately (unchanged behaviour, no FOUC).

FORMS-24971 — Font CLS prevention

  • styles/fonts.css: font-display: optional for all 4 Roboto @font-face rules — eliminates layout shift from late font swap.
  • head.html: preload for styles.css and roboto-regular.woff2 to close the parse-to-fetch gap.

FORMS-24975 — Lazy panel rendering

  • Opt-in via formDef.properties.lazyRendering = true.
  • Defers initial render of inactive wizard panels, hidden panels, and hidden non-panel field decorators — wrappers are still appended (so fieldChanged events resolve them) but children/decorators run only when the field becomes active or visible.
  • Reconciles against the live model in loadRuleEngine to handle rule-driven visibility post-restore.
  • No behaviour change for existing forms (flag absent).

Test plan

  • npm run lint — no new errors
  • npm run test:unit — all 257 tests pass (includes new functionRegistration.test.js)
  • npm run build — produces form-bundle.min.js and functions.min.js
  • npm run build:custom-functions -- --functions blocks/form/functions.js — single bundle mode
  • npm run build:custom-functions -- --functions <path> --form-json <path> — eager/lazy split, updates functions-registry.json
  • Verify a form without lazyRendering renders identically to before
  • Verify a form with lazyRendering: true defers inactive wizard panels
  • Verify CSS deferred loading when <name>-critical.css companion exists
  • Verify font CLS eliminated (Lighthouse CLS score)

🤖 Generated with Claude Code

dmaurya929 and others added 3 commits May 26, 2026 12:13
…module hops

Port the HDFC-validated bundle optimization to the boilerplate so all downstream
EDS Forms projects inherit the performance gains:
- 12 JS fetches → 1 bundle + 2 shims (−11 requests)
- 5 serial import hops → 2 hops (parallel preload)
- Worker startup: −31% (afb-runtime.min.js instead of full source)

New files: rollup/form.rollup.config.js, blocks/form/form.source.js,
blocks/form/rules/index.source.js, scripts/swap-shims.js
Modified: form.js and rules/index.js → 1-line shims; head.html adds 5 modulepreloads;
package.json adds build/build:dev/build:form-bundle scripts; pre-commit shim guard added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FORMS-24970 — Critical CSS split convention:
- loadFormCustomStyles now probes for <name>-critical.css via HEAD request.
  If the companion file exists, critical CSS loads immediately and the full
  stylesheet is deferred to window.load (post-LCP) via deferLoadCSS.
  If no companion file exists, full CSS loads immediately (old behaviour, no FOUC).
- Any form opting into the split ships <name>-critical.css alongside properties.style.

FORMS-24971 — Font CLS prevention:
- styles/fonts.css: font-display swap → optional for all 4 Roboto @font-face rules.
  Metric-matched fallback faces (roboto-fallback, roboto-condensed-fallback with
  size-adjust) already in styles.css make the swap visually imperceptible.
- head.html: preload for styles.css (eliminates parse-to-fetch gap) and
  roboto-regular.woff2 (ensures primary weight is available before LCP with optional).

Tests: updated custom-styles.test.js to mock HEAD requests as Promise.resolve({ok:false})
and flush the async probe chain before assertions. All 249 tests pass.

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

When enabled, defers initial render of inactive wizard panels, hidden panels,
and hidden non-panel field componentDecorators. Wrappers are still appended
(so fieldChanged events resolve them) but children/decorators run only when
the field becomes active or visible. Reconciles against the live model in
loadRuleEngine to handle rule-driven visibility post-restore.

Opt-in via formDef.properties.lazyRendering = true. Existing forms and the
public boilerplate's component tests are unchanged when the flag is absent.

- form.js: generateFormRendition accepts options={lazyPanels,lazyComponents};
  createForm reads formDef.properties.lazyRendering and conditionally creates
  the Maps + attaches them to form._lazyPanels/_lazyComponents.
- rules/index.js:
  - getLivePanelState queries live model so deferred panels render with
    post-rules state, not stale init-time fieldData.
  - handleActiveChild triggers lazy panel render on wizard tab activation.
  - case 'visible': dedup no-op guard; lazy component decorator-then-show;
    lazy panel pre-render wait or render-now/queue.
  - loadRuleEngine pre-renders lazy panels visible=true in live model
    (via _preRenderPromises), flushes _pendingLazyRenders, runs thunks for
    live-visible components, flushes _pendingLazyComponents.

Ported from forms-engine d8b6ff4f (HDFC fork) — adapted for the public
boilerplate (no ruleEngine.js shim; worker already has property-sync;
subscribe already fires immediately on registered formModels).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aem-code-sync

aem-code-sync Bot commented May 26, 2026

Copy link
Copy Markdown

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

  • Re-run all PSI checks
  • Re-run failed PSI checks
  • Re-sync branch
Commits

dmaurya929 and others added 2 commits May 28, 2026 11:18
Shims (form.js, rules/index.js) only re-export the public surface
(DELAY_MS, createForm, subscribe…) from form-bundle.min.js, which is
a browser bundle and unavailable in the Node.js Mocha environment.
Tests that import loadRuleEngine, generateFormRendition, fieldChanged,
etc. must import from the source files directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds opt-in build tooling that splits a form's custom functions into an eager
bundle (loaded at init) and a lazy bundle (deferred to the 3s mark) to remove
non-critical JS from the critical path.

- scripts/build-custom-functions.js — analyses form JSON / live page for
  load-time vs interaction functions, backs up the source, writes eager
  (.min.js) and lazy (-lazy.min.js) bundles, updates functions-registry.json,
  and replaces the entry file with a transparent shim
- rollup/custom-functions.rollup.config.js — per-form rollup pipeline; reads
  split manifest from functions-registry.json, inlines eager impls, stubs
  lazy fns, and exports loadLazyBundle for the 3s warmer
- rollup/functions.rollup.config.js — builds the OOTB functions.min.js bundles
- functionRegistration.js — modulepreloads eager bundle + speculative prefetch
  for lazy bundle; stores loadLazyBundle on window.hlx when split is active
- scripts.js — calls window.hlx.loadLazyBundle() at 3s mark
- package.json — adds build:functions and build:custom-functions scripts
- test/unit/functionRegistration.test.js — unit tests for preload and split
  detection behaviour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@aem-code-sync aem-code-sync Bot temporarily deployed to perf/combined-performance-enhancements May 29, 2026 08:49 Inactive
@dmaurya929 dmaurya929 changed the title perf: combined performance enhancements (FORMS-24968/24970/24971/24975) perf: combined performance enhancements (FORMS-24968/24969/24970/24971/24975) May 29, 2026
dmaurya929 and others added 5 commits June 2, 2026 11:42
Port model bundle upgrades from eds-forms-perf. Key change in af-core:
_pendingViewEvents queue — custom events fired before a lazy component's
subscriber registers are queued and replayed on subscribe(), fixing cases
where rule-triggered events were silently dropped on lazy panels.
af-formatters adds a calendar date validation guard (rejects e.g. Feb 30).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two missed-event cases when a modal panel's visible=false at load time:

1. modal.js subscribe callback: fires after the triggering visible=true has
   already been processed, so fieldModel.subscribe only catches future
   events. Added fieldModel.visible check at subscribe time to catch the
   missed open.

2. index.source.js fieldChanged: when a rule sets visible=true on a panel
   wrapping a <dialog>, call dialog.showModal() and add modal-open class —
   previously only the close path was handled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Custom components subscribe callbacks run once at decoration time and
query nested elements via querySelector. If child panels were deferred via
_lazyPanels, those elements wouldn't be in the DOM yet, breaking the
callbacks.

For custom components that have nested panel children, pass lazyPanels:null
to the recursive generateFormRendition call so their children render
immediately instead of being deferred.

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

Rollup outputs {name}-bundle-eager.min.js and {name}-bundle-lazy.min.js but
preloadFunctionScripts was computing {name}.min.js (eager modulepreload) and
{name}-lazy.min.js (lazy prefetch) — both wrong, causing 404s.

Also unifies --form flag in build-custom-functions.js (replaces --page /
--form-json with a single flag that auto-detects EDS page URL, .model.json
URL, or local file path) and updates rollup config output filenames to
match. Updates unit test assertions to the new naming convention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Picks up: custom component child-panel eager-render fix (form.source.js),
lazy modal showModal via fieldChanged (index.source.js), and the
getCustomComponents import added to form.source.js.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@aem-code-sync aem-code-sync Bot temporarily deployed to perf/combined-performance-enhancements June 2, 2026 06:14 Inactive
dmaurya929 and others added 5 commits June 3, 2026 12:53
Refactor parseAnnotations regexes to require a JSDoc /** ... */ block
delimiter and make the export keyword optional. This supports block-export
syntax (export { funcName } at EOF) in addition to inline exports.

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

Both modules are loaded independently via modulepreload so they arrive in
the browser cache before form-bundle executes. Inlining them duplicates
parse cost and inflates the bundle; externalizing removes both from the
form-bundle output and lets the browser share a single parsed copy.

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

When Worker is available, fire the worker init and custom-function
registration concurrently via Promise.all — both are independent and
previously ran sequentially, stalling form render while functions loaded.
Keep sequential fallback for environments without Worker support.

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

- Move aem.js + scripts.js before modulepreload hints so the browser
  dispatches the critical script fetches first
- Remove afb-formatters.min.js preload (discovered transitively; no
  benefit after bundle externalization)
- Add util.js + functionRegistration.js preloads — both are now external
  to form-bundle and need explicit wave-1 hints to avoid late fetches

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

Regenerated bundles now import util.js and functionRegistration.js as
external modules instead of inlining them. Net reduction ~470 lines in
form-bundle.js; form-bundle.min.js updated accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

1 participant