@atlaskit/react-ufo 3.14.5 → 3.14.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cjs/vc/index.js +39 -6
  3. package/dist/cjs/vc/vc-observer/index.js +10 -2
  4. package/dist/cjs/vc/vc-observer/observers/index.js +12 -7
  5. package/dist/cjs/vc/vc-observer/observers/ssr-placeholders/index.js +76 -40
  6. package/dist/cjs/vc/vc-observer-new/index.js +84 -0
  7. package/dist/cjs/vc/vc-observer-new/viewport-observer/index.js +227 -69
  8. package/dist/cjs/vc/vc-observer-new/viewport-observer/mutation-observer/index.js +103 -44
  9. package/dist/cjs/vc/vc-observer-new/viewport-observer/utils/is-in-vc-ignore-if-no-layout-shift-marker.js +17 -0
  10. package/dist/es2019/vc/index.js +37 -5
  11. package/dist/es2019/vc/vc-observer/index.js +8 -2
  12. package/dist/es2019/vc/vc-observer/observers/index.js +11 -5
  13. package/dist/es2019/vc/vc-observer/observers/ssr-placeholders/index.js +57 -26
  14. package/dist/es2019/vc/vc-observer-new/index.js +67 -1
  15. package/dist/es2019/vc/vc-observer-new/viewport-observer/index.js +105 -25
  16. package/dist/es2019/vc/vc-observer-new/viewport-observer/mutation-observer/index.js +44 -8
  17. package/dist/es2019/vc/vc-observer-new/viewport-observer/utils/is-in-vc-ignore-if-no-layout-shift-marker.js +11 -0
  18. package/dist/esm/vc/index.js +39 -6
  19. package/dist/esm/vc/vc-observer/index.js +10 -2
  20. package/dist/esm/vc/vc-observer/observers/index.js +12 -7
  21. package/dist/esm/vc/vc-observer/observers/ssr-placeholders/index.js +76 -40
  22. package/dist/esm/vc/vc-observer-new/index.js +84 -0
  23. package/dist/esm/vc/vc-observer-new/viewport-observer/index.js +227 -69
  24. package/dist/esm/vc/vc-observer-new/viewport-observer/mutation-observer/index.js +103 -44
  25. package/dist/esm/vc/vc-observer-new/viewport-observer/utils/is-in-vc-ignore-if-no-layout-shift-marker.js +11 -0
  26. package/dist/types/vc/index.d.ts +2 -0
  27. package/dist/types/vc/types.d.ts +2 -0
  28. package/dist/types/vc/vc-observer/index.d.ts +1 -0
  29. package/dist/types/vc/vc-observer/observers/index.d.ts +2 -0
  30. package/dist/types/vc/vc-observer/observers/ssr-placeholders/index.d.ts +6 -0
  31. package/dist/types/vc/vc-observer-new/index.d.ts +30 -0
  32. package/dist/types/vc/vc-observer-new/types.d.ts +1 -1
  33. package/dist/types/vc/vc-observer-new/viewport-observer/index.d.ts +5 -1
  34. package/dist/types/vc/vc-observer-new/viewport-observer/mutation-observer/index.d.ts +2 -0
  35. package/dist/types/vc/vc-observer-new/viewport-observer/utils/is-in-vc-ignore-if-no-layout-shift-marker.d.ts +6 -0
  36. package/dist/types-ts4.5/vc/index.d.ts +2 -0
  37. package/dist/types-ts4.5/vc/types.d.ts +2 -0
  38. package/dist/types-ts4.5/vc/vc-observer/index.d.ts +1 -0
  39. package/dist/types-ts4.5/vc/vc-observer/observers/index.d.ts +2 -0
  40. package/dist/types-ts4.5/vc/vc-observer/observers/ssr-placeholders/index.d.ts +6 -0
  41. package/dist/types-ts4.5/vc/vc-observer-new/index.d.ts +30 -0
  42. package/dist/types-ts4.5/vc/vc-observer-new/types.d.ts +1 -1
  43. package/dist/types-ts4.5/vc/vc-observer-new/viewport-observer/index.d.ts +5 -1
  44. package/dist/types-ts4.5/vc/vc-observer-new/viewport-observer/mutation-observer/index.d.ts +2 -0
  45. package/dist/types-ts4.5/vc/vc-observer-new/viewport-observer/utils/is-in-vc-ignore-if-no-layout-shift-marker.d.ts +6 -0
  46. package/package.json +8 -3
@@ -495,7 +495,8 @@ export class VCObserver {
495
495
  this.oldDomUpdatesEnabled = options.oldDomUpdates || false;
496
496
  const {
497
497
  ssrEnablePageLayoutPlaceholder,
498
- disableSizeAndPositionCheck
498
+ disableSizeAndPositionCheck,
499
+ ssrPlaceholderHandler
499
500
  } = options;
500
501
  this.observers = new Observers({
501
502
  selectorConfig: options.selectorConfig || {
@@ -508,7 +509,8 @@ export class VCObserver {
508
509
  SSRConfig: {
509
510
  enablePageLayoutPlaceholder: ssrEnablePageLayoutPlaceholder || false,
510
511
  disableSizeAndPositionCheck: disableSizeAndPositionCheck
511
- }
512
+ },
513
+ ssrPlaceholderHandler: ssrPlaceholderHandler
512
514
  });
513
515
  this.heatmap = !isVCRevisionEnabled('fy25.01') ? [] : this.getCleanHeatmap();
514
516
  this.heatmapNext = this.getCleanHeatmap();
@@ -654,6 +656,10 @@ export class VCObserver {
654
656
  setReactRootRenderStop(stopTime = performance.now()) {
655
657
  this.observers.setReactRootRenderStop(stopTime);
656
658
  }
659
+ collectSSRPlaceholders() {
660
+ // This is handled by the shared SSRPlaceholderHandlers in VCObserverWrapper
661
+ // Individual observers don't need to implement this
662
+ }
657
663
  setAbortReason(abort, timestamp, info = '') {
658
664
  if (this.abortReason.reason === null || this.abortReason.blocking === false) {
659
665
  this.abortReason.reason = abort;
@@ -21,7 +21,6 @@ function isElementVisible(target) {
21
21
  }
22
22
  export class Observers {
23
23
  constructor(opts) {
24
- var _opts$SSRConfig, _opts$SSRConfig2;
25
24
  _defineProperty(this, "observedMutations", new WeakMap());
26
25
  _defineProperty(this, "elementsInView", new Set());
27
26
  _defineProperty(this, "callbacks", new Set());
@@ -61,10 +60,17 @@ export class Observers {
61
60
  };
62
61
  this.intersectionObserver = this.getIntersectionObserver();
63
62
  this.mutationObserver = this.getMutationObserver();
64
- this.ssrPlaceholderHandler = new SSRPlaceholderHandlers({
65
- enablePageLayoutPlaceholder: (_opts$SSRConfig = opts.SSRConfig) === null || _opts$SSRConfig === void 0 ? void 0 : _opts$SSRConfig.enablePageLayoutPlaceholder,
66
- disableSizeAndPositionCheck: (_opts$SSRConfig2 = opts.SSRConfig) === null || _opts$SSRConfig2 === void 0 ? void 0 : _opts$SSRConfig2.disableSizeAndPositionCheck
67
- });
63
+
64
+ // Use shared SSR placeholder handler if provided, otherwise create new one
65
+ if (opts.ssrPlaceholderHandler) {
66
+ this.ssrPlaceholderHandler = opts.ssrPlaceholderHandler;
67
+ } else {
68
+ var _opts$SSRConfig, _opts$SSRConfig2;
69
+ this.ssrPlaceholderHandler = new SSRPlaceholderHandlers({
70
+ enablePageLayoutPlaceholder: (_opts$SSRConfig = opts.SSRConfig) === null || _opts$SSRConfig === void 0 ? void 0 : _opts$SSRConfig.enablePageLayoutPlaceholder,
71
+ disableSizeAndPositionCheck: (_opts$SSRConfig2 = opts.SSRConfig) === null || _opts$SSRConfig2 === void 0 ? void 0 : _opts$SSRConfig2.disableSizeAndPositionCheck
72
+ });
73
+ }
68
74
  }
69
75
  isBrowserSupported() {
70
76
  return typeof window.IntersectionObserver === 'function' && typeof window.MutationObserver === 'function';
@@ -96,32 +96,8 @@ export class SSRPlaceholderHandlers {
96
96
  this.disableSizeAndPositionCheck = disableSizeAndPositionCheck;
97
97
  if (window.document) {
98
98
  try {
99
- const selector = this.enablePageLayoutPlaceholder ? '[data-ssr-placeholder],[data-testid="page-layout.root"]' : '[data-ssr-placeholder]';
100
- const existingElements = document.querySelectorAll(selector);
101
- existingElements.forEach(el => {
102
- const placeholderId = el instanceof HTMLElement && this.getPlaceholderId(el);
103
- if (placeholderId) {
104
- var _window$__SSR_PLACEHO, _this$intersectionObs2;
105
- let width = -1;
106
- let height = -1;
107
- let x = -1;
108
- let y = -1;
109
- const boundingClientRect = (_window$__SSR_PLACEHO = window.__SSR_PLACEHOLDERS_DIMENSIONS__) === null || _window$__SSR_PLACEHO === void 0 ? void 0 : _window$__SSR_PLACEHO[placeholderId];
110
- if (boundingClientRect) {
111
- width = boundingClientRect.width;
112
- height = boundingClientRect.height;
113
- x = boundingClientRect.x;
114
- y = boundingClientRect.y;
115
- }
116
- this.staticPlaceholders.set(placeholderId, {
117
- width,
118
- height,
119
- x,
120
- y
121
- });
122
- (_this$intersectionObs2 = this.intersectionObserver) === null || _this$intersectionObs2 === void 0 ? void 0 : _this$intersectionObs2.observe(el);
123
- }
124
- });
99
+ // Collect initial placeholders using SSR dimensions
100
+ this.collectPlaceholdersInternal();
125
101
  } catch (e) {} finally {
126
102
  delete window.__SSR_PLACEHOLDERS_DIMENSIONS__;
127
103
  }
@@ -133,6 +109,61 @@ export class SSRPlaceholderHandlers {
133
109
  this.getSizeCallbacks = new Map();
134
110
  this.reactValidateCallbacks = new Map();
135
111
  }
112
+ collectPlaceholdersInternal() {
113
+ const selector = this.enablePageLayoutPlaceholder ? '[data-ssr-placeholder],[data-testid="page-layout.root"]' : '[data-ssr-placeholder]';
114
+ const existingElements = document.querySelectorAll(selector);
115
+ existingElements.forEach(el => {
116
+ const placeholderId = el instanceof HTMLElement && this.getPlaceholderId(el);
117
+ if (placeholderId && !this.staticPlaceholders.has(placeholderId)) {
118
+ var _window$__SSR_PLACEHO, _this$intersectionObs2;
119
+ let width = -1;
120
+ let height = -1;
121
+ let x = -1;
122
+ let y = -1;
123
+
124
+ // Use SSR dimensions from window global if available
125
+ const boundingClientRect = (_window$__SSR_PLACEHO = window.__SSR_PLACEHOLDERS_DIMENSIONS__) === null || _window$__SSR_PLACEHO === void 0 ? void 0 : _window$__SSR_PLACEHO[placeholderId];
126
+ if (boundingClientRect) {
127
+ width = boundingClientRect.width;
128
+ height = boundingClientRect.height;
129
+ x = boundingClientRect.x;
130
+ y = boundingClientRect.y;
131
+ } else {
132
+ // Fallback to current bounding rect if SSR dimensions not available
133
+ const rect = el.getBoundingClientRect();
134
+ width = rect.width;
135
+ height = rect.height;
136
+ x = rect.x;
137
+ y = rect.y;
138
+ }
139
+ this.staticPlaceholders.set(placeholderId, {
140
+ width,
141
+ height,
142
+ x,
143
+ y
144
+ });
145
+ (_this$intersectionObs2 = this.intersectionObserver) === null || _this$intersectionObs2 === void 0 ? void 0 : _this$intersectionObs2.observe(el);
146
+ }
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Added this method to be utilised for testing purposes.
152
+ * In production it collection placeholder should only happens on constructor
153
+ */
154
+ collectExistingPlaceholders() {
155
+ if (!window.document) {
156
+ return;
157
+ }
158
+ try {
159
+ // Collect placeholders using SSR dimensions or fallback to live dimensions
160
+ this.collectPlaceholdersInternal();
161
+ } catch (e) {
162
+ // Silently fail if there are any issues
163
+ } finally {
164
+ delete window.__SSR_PLACEHOLDERS_DIMENSIONS__;
165
+ }
166
+ }
136
167
  isPlaceholder(element) {
137
168
  return Boolean(this.getPlaceholderId(element));
138
169
  }
@@ -1,5 +1,6 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  import { fg } from '@atlaskit/platform-feature-flags';
3
+ import { SSRPlaceholderHandlers } from '../vc-observer/observers/ssr-placeholders';
3
4
  import EntriesTimeline from './entries-timeline';
4
5
  import getElementName from './get-element-name';
5
6
  import VCCalculator_FY25_03 from './metric-calculator/fy25_03';
@@ -7,6 +8,11 @@ import getViewportHeight from './metric-calculator/utils/get-viewport-height';
7
8
  import getViewportWidth from './metric-calculator/utils/get-viewport-width';
8
9
  import ViewportObserver from './viewport-observer';
9
10
  import WindowEventObserver from './window-event-observer';
11
+ const SSRState = {
12
+ normal: 1,
13
+ waitingForFirstRender: 2,
14
+ ignoring: 3
15
+ };
10
16
  const DEFAULT_SELECTOR_CONFIG = {
11
17
  id: false,
12
18
  testId: true,
@@ -19,9 +25,31 @@ export default class VCObserverNew {
19
25
  var _config$isPostInterac, _config$selectorConfi;
20
26
  _defineProperty(this, "viewportObserver", null);
21
27
  _defineProperty(this, "windowEventObserver", null);
28
+ // SSR related properties
29
+ _defineProperty(this, "ssrPlaceholderHandler", null);
30
+ _defineProperty(this, "ssr", {
31
+ state: SSRState.normal,
32
+ reactRootElement: null,
33
+ renderStart: -1,
34
+ renderStop: -1
35
+ });
22
36
  this.entriesTimeline = new EntriesTimeline();
23
37
  this.isPostInteraction = (_config$isPostInterac = config.isPostInteraction) !== null && _config$isPostInterac !== void 0 ? _config$isPostInterac : false;
24
38
  this.selectorConfig = (_config$selectorConfi = config.selectorConfig) !== null && _config$selectorConfi !== void 0 ? _config$selectorConfi : DEFAULT_SELECTOR_CONFIG;
39
+
40
+ // Use shared SSR placeholder handler if provided, otherwise create new one if feature flag is enabled
41
+ if (config.ssrPlaceholderHandler) {
42
+ this.ssrPlaceholderHandler = config.ssrPlaceholderHandler;
43
+ } else {
44
+ var _config$SSRConfig$ena, _config$SSRConfig, _config$SSRConfig$dis, _config$SSRConfig2;
45
+ this.ssrPlaceholderHandler = new SSRPlaceholderHandlers({
46
+ enablePageLayoutPlaceholder: (_config$SSRConfig$ena = (_config$SSRConfig = config.SSRConfig) === null || _config$SSRConfig === void 0 ? void 0 : _config$SSRConfig.enablePageLayoutPlaceholder) !== null && _config$SSRConfig$ena !== void 0 ? _config$SSRConfig$ena : false,
47
+ disableSizeAndPositionCheck: (_config$SSRConfig$dis = (_config$SSRConfig2 = config.SSRConfig) === null || _config$SSRConfig2 === void 0 ? void 0 : _config$SSRConfig2.disableSizeAndPositionCheck) !== null && _config$SSRConfig$dis !== void 0 ? _config$SSRConfig$dis : {
48
+ v: false,
49
+ h: false
50
+ }
51
+ });
52
+ }
25
53
  this.viewportObserver = new ViewportObserver({
26
54
  onChange: onChangeArg => {
27
55
  const {
@@ -51,7 +79,10 @@ export default class VCObserverNew {
51
79
  newValue: mutationData === null || mutationData === void 0 ? void 0 : mutationData.newValue
52
80
  }
53
81
  });
54
- }
82
+ },
83
+ // Pass SSR context to ViewportObserver
84
+ getSSRState: () => this.getSSRState(),
85
+ getSSRPlaceholderHandler: () => this.getSSRPlaceholderHandler()
55
86
  });
56
87
  this.windowEventObserver = new WindowEventObserver({
57
88
  onEvent: ({
@@ -72,6 +103,14 @@ export default class VCObserverNew {
72
103
  startTime
73
104
  }) {
74
105
  var _this$viewportObserve, _window, _this$windowEventObse;
106
+ // Reset SSR state on start (matches old VCObserver behavior)
107
+ this.ssr = {
108
+ state: SSRState.normal,
109
+ reactRootElement: null,
110
+ // Reset to null (matches old VCObserver)
111
+ renderStart: -1,
112
+ renderStop: -1
113
+ };
75
114
  (_this$viewportObserve = this.viewportObserver) === null || _this$viewportObserve === void 0 ? void 0 : _this$viewportObserve.start();
76
115
  if ((_window = window) !== null && _window !== void 0 && _window.__SSR_ABORT_LISTENERS__ && fg('platform_ufo_vc_observer_new_ssr_abort_listener')) {
77
116
  const abortListeners = window.__SSR_ABORT_LISTENERS__;
@@ -97,6 +136,33 @@ export default class VCObserverNew {
97
136
  var _this$viewportObserve2, _this$windowEventObse2;
98
137
  (_this$viewportObserve2 = this.viewportObserver) === null || _this$viewportObserve2 === void 0 ? void 0 : _this$viewportObserve2.stop();
99
138
  (_this$windowEventObse2 = this.windowEventObserver) === null || _this$windowEventObse2 === void 0 ? void 0 : _this$windowEventObse2.stop();
139
+
140
+ // Clear SSR state on stop (matches old VCObserver behavior)
141
+ this.ssr.reactRootElement = null;
142
+ }
143
+
144
+ // SSR related methods
145
+ setReactRootElement(element) {
146
+ this.ssr.reactRootElement = element;
147
+ }
148
+ setReactRootRenderStart(startTime = performance.now()) {
149
+ this.ssr.renderStart = startTime;
150
+ this.ssr.state = SSRState.waitingForFirstRender;
151
+ }
152
+ setReactRootRenderStop(stopTime = performance.now()) {
153
+ this.ssr.renderStop = stopTime;
154
+ }
155
+ collectSSRPlaceholders() {
156
+ // This is handled by the shared SSRPlaceholderHandlers in VCObserverWrapper
157
+ // Individual observers don't need to implement this
158
+ }
159
+
160
+ // Internal methods for ViewportObserver to access SSR state
161
+ getSSRState() {
162
+ return this.ssr;
163
+ }
164
+ getSSRPlaceholderHandler() {
165
+ return this.ssrPlaceholderHandler;
100
166
  }
101
167
  addSSR(ssr) {
102
168
  this.entriesTimeline.push({
@@ -1,4 +1,5 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import { fg } from '@atlaskit/platform-feature-flags';
2
3
  import { isContainedWithinMediaWrapper } from '../../vc-observer/media-wrapper/vc-utils';
3
4
  import isNonVisualStyleMutation from '../../vc-observer/observers/non-visual-styles/is-non-visual-style-mutation';
4
5
  import { RLLPlaceholderHandlers } from '../../vc-observer/observers/rll-placeholders';
@@ -6,13 +7,14 @@ import { createIntersectionObserver } from './intersection-observer';
6
7
  import createMutationObserver from './mutation-observer';
7
8
  import createPerformanceObserver from './performance-observer';
8
9
  import { checkThirdPartySegmentWithIgnoreReason, createMutationTypeWithIgnoredReason } from './utils/get-component-name-and-child-props';
10
+ import isInVCIgnoreIfNoLayoutShiftMarker from './utils/is-in-vc-ignore-if-no-layout-shift-marker';
9
11
  function isElementVisible(element) {
10
12
  if (!(element instanceof HTMLElement)) {
11
13
  return true;
12
14
  }
13
15
  try {
14
16
  const visible = element.checkVisibility({
15
- // @ts-expect-error
17
+ // @ts-ignore - visibilityProperty may not exist in all TS environments
16
18
  visibilityProperty: true,
17
19
  contentVisibilityAuto: true,
18
20
  opacityProperty: true
@@ -36,21 +38,33 @@ function sameRectDimensions(a, b) {
36
38
  return a.width === b.width && a.height === b.height && a.x === b.x && a.y === b.y;
37
39
  }
38
40
  const createElementMutationsWatcher = removedNodeRects => ({
41
+ target,
39
42
  rect
40
43
  }) => {
44
+ const isNoLsMarkerEnabled = fg('platform_vc_ignore_no_ls_mutation_marker');
45
+ const isInIgnoreLsMarker = isInVCIgnoreIfNoLayoutShiftMarker(target);
46
+ if (!isInIgnoreLsMarker && isNoLsMarkerEnabled) {
47
+ return 'mutation:element';
48
+ }
41
49
  const isRLLPlaceholder = RLLPlaceholderHandlers.getInstance().isRLLPlaceholderHydration(rect);
42
- if (isRLLPlaceholder) {
50
+ if (isRLLPlaceholder && (!isNoLsMarkerEnabled || isInIgnoreLsMarker)) {
43
51
  return 'mutation:rll-placeholder';
44
52
  }
45
53
  const wasDeleted = removedNodeRects.some(nr => sameRectDimensions(nr, rect));
46
- if (wasDeleted) {
54
+ // When fg('platform_vc_ignore_no_ls_mutation_marker') is not enabled,
55
+ // no layout shift mutation is excluded as per existing fy25.03 logic
56
+ if (wasDeleted && (!isNoLsMarkerEnabled || isInIgnoreLsMarker)) {
47
57
  return 'mutation:element-replacement';
48
58
  }
49
59
  return 'mutation:element';
50
60
  };
51
61
  export default class ViewportObserver {
62
+ // SSR context functions
63
+
52
64
  constructor({
53
- onChange
65
+ onChange,
66
+ getSSRState,
67
+ getSSRPlaceholderHandler
54
68
  }) {
55
69
  _defineProperty(this, "handleIntersectionEntry", ({
56
70
  target,
@@ -75,9 +89,11 @@ export default class ViewportObserver {
75
89
  mutationData
76
90
  });
77
91
  });
78
- _defineProperty(this, "handleChildListMutation", ({
92
+ _defineProperty(this, "handleChildListMutation", async ({
93
+ target,
79
94
  addedNodes,
80
- removedNodes
95
+ removedNodes,
96
+ timestamp
81
97
  }) => {
82
98
  const removedNodeRects = removedNodes.map(ref => {
83
99
  const n = ref.deref();
@@ -86,11 +102,66 @@ export default class ViewportObserver {
86
102
  }
87
103
  return this.mapVisibleNodeRects.get(n);
88
104
  });
89
- addedNodes.forEach(addedNodeRef => {
90
- var _this$intersectionObs4;
105
+ const targetNode = target.deref();
106
+ for (const addedNodeRef of addedNodes) {
107
+ var _this$intersectionObs8;
91
108
  const addedNode = addedNodeRef.deref();
92
109
  if (!addedNode) {
93
- return;
110
+ continue;
111
+ }
112
+
113
+ // SSR hydration logic
114
+ if (this.getSSRState && fg('platform_ufo_vc_v3_ssr_placeholder')) {
115
+ const ssrState = this.getSSRState();
116
+ const SSRStateEnum = {
117
+ normal: 1,
118
+ waitingForFirstRender: 2,
119
+ ignoring: 3
120
+ };
121
+ if (ssrState.state === SSRStateEnum.waitingForFirstRender && timestamp > ssrState.renderStart && targetNode === ssrState.reactRootElement) {
122
+ var _this$intersectionObs;
123
+ ssrState.state = SSRStateEnum.ignoring;
124
+ if (ssrState.renderStop === -1) {
125
+ // arbitrary 500ms DOM update window
126
+ ssrState.renderStop = timestamp + 500;
127
+ }
128
+ (_this$intersectionObs = this.intersectionObserver) === null || _this$intersectionObs === void 0 ? void 0 : _this$intersectionObs.watchAndTag(addedNode, 'ssr-hydration');
129
+ continue;
130
+ }
131
+ if (ssrState.state === SSRStateEnum.ignoring && timestamp > ssrState.renderStart && targetNode === ssrState.reactRootElement) {
132
+ if (timestamp <= ssrState.renderStop) {
133
+ var _this$intersectionObs2;
134
+ (_this$intersectionObs2 = this.intersectionObserver) === null || _this$intersectionObs2 === void 0 ? void 0 : _this$intersectionObs2.watchAndTag(addedNode, 'ssr-hydration');
135
+ continue;
136
+ } else {
137
+ ssrState.state = SSRStateEnum.normal;
138
+ }
139
+ }
140
+ }
141
+
142
+ // SSR placeholder logic - check and handle with await
143
+ if (this.getSSRPlaceholderHandler && fg('platform_ufo_vc_v3_ssr_placeholder')) {
144
+ const ssrPlaceholderHandler = this.getSSRPlaceholderHandler();
145
+ if (ssrPlaceholderHandler) {
146
+ if (ssrPlaceholderHandler.isPlaceholder(addedNode) || ssrPlaceholderHandler.isPlaceholderIgnored(addedNode)) {
147
+ const result = await ssrPlaceholderHandler.checkIfExistedAndSizeMatching(addedNode);
148
+ if (result !== false) {
149
+ var _this$intersectionObs3;
150
+ (_this$intersectionObs3 = this.intersectionObserver) === null || _this$intersectionObs3 === void 0 ? void 0 : _this$intersectionObs3.watchAndTag(addedNode, 'mutation:ssr-placeholder');
151
+ continue;
152
+ }
153
+ // If result is false, continue to normal mutation logic below
154
+ }
155
+ if (ssrPlaceholderHandler.isPlaceholderReplacement(addedNode) || ssrPlaceholderHandler.isPlaceholderIgnored(addedNode)) {
156
+ const result = await ssrPlaceholderHandler.validateReactComponentMatchToPlaceholder(addedNode);
157
+ if (result !== false) {
158
+ var _this$intersectionObs4;
159
+ (_this$intersectionObs4 = this.intersectionObserver) === null || _this$intersectionObs4 === void 0 ? void 0 : _this$intersectionObs4.watchAndTag(addedNode, 'mutation:ssr-placeholder');
160
+ continue;
161
+ }
162
+ // If result is false, continue to normal mutation logic below
163
+ }
164
+ }
94
165
  }
95
166
  const sameDeletedNode = removedNodes.find(ref => {
96
167
  const n = ref.deref();
@@ -99,28 +170,33 @@ export default class ViewportObserver {
99
170
  }
100
171
  return n.isEqualNode(addedNode);
101
172
  });
102
- if (sameDeletedNode) {
103
- var _this$intersectionObs;
104
- (_this$intersectionObs = this.intersectionObserver) === null || _this$intersectionObs === void 0 ? void 0 : _this$intersectionObs.watchAndTag(addedNode, 'mutation:remount');
105
- return;
173
+ const isInIgnoreLsMarker = isInVCIgnoreIfNoLayoutShiftMarker(addedNode);
174
+ const isNoLsMarkerEnabled = fg('platform_vc_ignore_no_ls_mutation_marker');
175
+
176
+ // When fg('platform_vc_ignore_no_ls_mutation_marker') is not enabled,
177
+ // no layout shift mutation is excluded as per existing fy25.03 logic
178
+ if (sameDeletedNode && (!isNoLsMarkerEnabled || isInIgnoreLsMarker)) {
179
+ var _this$intersectionObs5;
180
+ (_this$intersectionObs5 = this.intersectionObserver) === null || _this$intersectionObs5 === void 0 ? void 0 : _this$intersectionObs5.watchAndTag(addedNode, 'mutation:remount');
181
+ continue;
106
182
  }
107
183
  if (isContainedWithinMediaWrapper(addedNode)) {
108
- var _this$intersectionObs2;
109
- (_this$intersectionObs2 = this.intersectionObserver) === null || _this$intersectionObs2 === void 0 ? void 0 : _this$intersectionObs2.watchAndTag(addedNode, 'mutation:media');
110
- return;
184
+ var _this$intersectionObs6;
185
+ (_this$intersectionObs6 = this.intersectionObserver) === null || _this$intersectionObs6 === void 0 ? void 0 : _this$intersectionObs6.watchAndTag(addedNode, 'mutation:media');
186
+ continue;
111
187
  }
112
188
  const {
113
189
  isWithinThirdPartySegment,
114
190
  ignoredReason
115
191
  } = checkThirdPartySegmentWithIgnoreReason(addedNode);
116
192
  if (isWithinThirdPartySegment) {
117
- var _this$intersectionObs3;
193
+ var _this$intersectionObs7;
118
194
  const assignedReason = createMutationTypeWithIgnoredReason(ignoredReason || 'third-party-element');
119
- (_this$intersectionObs3 = this.intersectionObserver) === null || _this$intersectionObs3 === void 0 ? void 0 : _this$intersectionObs3.watchAndTag(addedNode, assignedReason);
120
- return;
195
+ (_this$intersectionObs7 = this.intersectionObserver) === null || _this$intersectionObs7 === void 0 ? void 0 : _this$intersectionObs7.watchAndTag(addedNode, assignedReason);
196
+ continue;
121
197
  }
122
- (_this$intersectionObs4 = this.intersectionObserver) === null || _this$intersectionObs4 === void 0 ? void 0 : _this$intersectionObs4.watchAndTag(addedNode, createElementMutationsWatcher(removedNodeRects));
123
- });
198
+ (_this$intersectionObs8 = this.intersectionObserver) === null || _this$intersectionObs8 === void 0 ? void 0 : _this$intersectionObs8.watchAndTag(addedNode, createElementMutationsWatcher(removedNodeRects));
199
+ }
124
200
  });
125
201
  _defineProperty(this, "handleAttributeMutation", ({
126
202
  target,
@@ -128,8 +204,8 @@ export default class ViewportObserver {
128
204
  oldValue,
129
205
  newValue
130
206
  }) => {
131
- var _this$intersectionObs5;
132
- (_this$intersectionObs5 = this.intersectionObserver) === null || _this$intersectionObs5 === void 0 ? void 0 : _this$intersectionObs5.watchAndTag(target, ({
207
+ var _this$intersectionObs9;
208
+ (_this$intersectionObs9 = this.intersectionObserver) === null || _this$intersectionObs9 === void 0 ? void 0 : _this$intersectionObs9.watchAndTag(target, ({
133
209
  target,
134
210
  rect
135
211
  }) => {
@@ -228,6 +304,10 @@ export default class ViewportObserver {
228
304
  this.intersectionObserver = null;
229
305
  this.mutationObserver = null;
230
306
  this.performanceObserver = null;
307
+
308
+ // Initialize SSR context functions
309
+ this.getSSRState = getSSRState;
310
+ this.getSSRPlaceholderHandler = getSSRPlaceholderHandler;
231
311
  }
232
312
  initializeObservers() {
233
313
  if (this.isStarted) {
@@ -265,12 +345,12 @@ export default class ViewportObserver {
265
345
  this.isStarted = true;
266
346
  }
267
347
  stop() {
268
- var _this$mutationObserve2, _this$intersectionObs6, _this$performanceObse2;
348
+ var _this$mutationObserve2, _this$intersectionObs0, _this$performanceObse2;
269
349
  if (!this.isStarted) {
270
350
  return;
271
351
  }
272
352
  (_this$mutationObserve2 = this.mutationObserver) === null || _this$mutationObserve2 === void 0 ? void 0 : _this$mutationObserve2.disconnect();
273
- (_this$intersectionObs6 = this.intersectionObserver) === null || _this$intersectionObs6 === void 0 ? void 0 : _this$intersectionObs6.disconnect();
353
+ (_this$intersectionObs0 = this.intersectionObserver) === null || _this$intersectionObs0 === void 0 ? void 0 : _this$intersectionObs0.disconnect();
274
354
  (_this$performanceObse2 = this.performanceObserver) === null || _this$performanceObse2 === void 0 ? void 0 : _this$performanceObse2.disconnect();
275
355
  this.isStarted = false;
276
356
  }
@@ -1,3 +1,5 @@
1
+ // Batched mutation data for performance optimization
2
+
1
3
  function createMutationObserver({
2
4
  onAttributeMutation,
3
5
  onChildListMutation,
@@ -7,9 +9,10 @@ function createMutationObserver({
7
9
  return null;
8
10
  }
9
11
  const mutationObserverCallback = mutations => {
10
- const addedNodes = [];
11
- const removedNodes = [];
12
12
  const targets = [];
13
+ // Use nested Maps for O(1) batching performance
14
+ // Short-lived Maps are safe since they're discarded after each callback
15
+ const batchedMutations = new Map();
13
16
  for (const mut of mutations) {
14
17
  if (!(mut.target instanceof HTMLElement)) {
15
18
  continue;
@@ -38,23 +41,56 @@ function createMutationObserver({
38
41
  continue;
39
42
  } else if (mut.type === 'childList') {
40
43
  var _mut$addedNodes, _mut$removedNodes;
44
+ // In chromium browser MutationRecord has timestamp field, which we should use.
45
+ const timestamp = Math.round(mut.timestamp || performance.now());
46
+
47
+ // Get or create timestamp bucket
48
+ let timestampBucket = batchedMutations.get(timestamp);
49
+ if (!timestampBucket) {
50
+ timestampBucket = new Map();
51
+ batchedMutations.set(timestamp, timestampBucket);
52
+ }
53
+
54
+ // Get or create target batch within timestamp bucket
55
+ let batch = timestampBucket.get(mut.target);
56
+ if (!batch) {
57
+ batch = {
58
+ target: new WeakRef(mut.target),
59
+ addedNodes: [],
60
+ removedNodes: [],
61
+ timestamp
62
+ };
63
+ timestampBucket.set(mut.target, batch);
64
+ }
65
+
66
+ // Accumulate added nodes
41
67
  ((_mut$addedNodes = mut.addedNodes) !== null && _mut$addedNodes !== void 0 ? _mut$addedNodes : []).forEach(node => {
42
68
  if (node instanceof HTMLElement) {
43
- addedNodes.push(new WeakRef(node));
69
+ batch.addedNodes.push(new WeakRef(node));
44
70
  }
45
71
  });
72
+
73
+ // Accumulate removed nodes
46
74
  ((_mut$removedNodes = mut.removedNodes) !== null && _mut$removedNodes !== void 0 ? _mut$removedNodes : []).forEach(node => {
47
75
  if (node instanceof HTMLElement) {
48
- removedNodes.push(new WeakRef(node));
76
+ batch.removedNodes.push(new WeakRef(node));
49
77
  }
50
78
  });
51
79
  }
52
80
  targets.push(mut.target);
53
81
  }
54
- onChildListMutation({
55
- addedNodes,
56
- removedNodes
57
- });
82
+
83
+ // Process all batched childList mutations
84
+ for (const timestampBucket of batchedMutations.values()) {
85
+ for (const batch of timestampBucket.values()) {
86
+ onChildListMutation({
87
+ target: batch.target,
88
+ addedNodes: batch.addedNodes,
89
+ removedNodes: batch.removedNodes,
90
+ timestamp: batch.timestamp
91
+ });
92
+ }
93
+ }
58
94
  onMutationFinished === null || onMutationFinished === void 0 ? void 0 : onMutationFinished({
59
95
  targets
60
96
  });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Check if the element is in the VC ignore if no layout shift marker.
3
+ * @param element - The element to check.
4
+ * @returns True if the element has the data-vc-ignore-if-no-layout-shift attribute == 'true or its parent has it, false otherwise.
5
+ */
6
+ export default function isInVCIgnoreIfNoLayoutShiftMarker(element) {
7
+ if (!element) {
8
+ return false;
9
+ }
10
+ return element.getAttribute('data-vc-ignore-if-no-layout-shift') === 'true' || isInVCIgnoreIfNoLayoutShiftMarker(element.parentElement);
11
+ }