@gemx-dev/heatmap-react 3.5.92-dev.8 → 3.5.92-dev.9

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.
@@ -1 +1 @@
1
- {"version":3,"file":"useHeatmapRender.d.ts","sourceRoot":"","sources":["../../../src/hooks/viz-render/useHeatmapRender.ts"],"names":[],"mappings":"AAiBA,UAAU,uBAAuB;IAC/B,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;CACtD;AAUD,eAAO,MAAM,gBAAgB,QAAO,uBA+DnC,CAAC"}
1
+ {"version":3,"file":"useHeatmapRender.d.ts","sourceRoot":"","sources":["../../../src/hooks/viz-render/useHeatmapRender.ts"],"names":[],"mappings":"AAgCA,UAAU,uBAAuB;IAC/B,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;CACtD;AAaD,eAAO,MAAM,gBAAgB,QAAO,uBAoEnC,CAAC"}
package/dist/esm/index.js CHANGED
@@ -3904,6 +3904,85 @@ function useVizLiveIframeMsg(options = {}) {
3904
3904
  };
3905
3905
  }
3906
3906
 
3907
+ /**
3908
+ * Performance tracker — measures render pipeline timings and stores results
3909
+ * in `window.__gemxPerf` for inspection in DevTools.
3910
+ *
3911
+ * Usage:
3912
+ * perf.startSession('render-1');
3913
+ * const t = perf.mark('viewport.run');
3914
+ * perf.measure('viewport.run', t);
3915
+ * perf.endSession();
3916
+ *
3917
+ * // In DevTools:
3918
+ * window.__gemxPerf.latest
3919
+ * window.__gemxPerf.sessions
3920
+ */
3921
+ const s = {
3922
+ enabled: true,
3923
+ current: null,
3924
+ sessions: [],
3925
+ maxSessions: 20,
3926
+ };
3927
+ // ── Functions ─────────────────────────────────────────────────────────────────
3928
+ function startSession(id) {
3929
+ if (!s.enabled)
3930
+ return;
3931
+ s.current = { id, startedAt: performance.now(), entries: [] };
3932
+ }
3933
+ function endSession() {
3934
+ if (!s.enabled || !s.current)
3935
+ return null;
3936
+ const session = s.current;
3937
+ session.total = performance.now() - session.startedAt;
3938
+ s.sessions = [session, ...s.sessions].slice(0, s.maxSessions);
3939
+ s.current = null;
3940
+ flush();
3941
+ return session;
3942
+ }
3943
+ /** Record a point-in-time mark. Returns `performance.now()` for use with measure(). */
3944
+ function mark$1(label) {
3945
+ const now = performance.now();
3946
+ if (s.enabled && s.current) {
3947
+ s.current.entries.push({ label, t: now - s.current.startedAt });
3948
+ }
3949
+ return now;
3950
+ }
3951
+ /** Record a duration from a previous mark() timestamp. */
3952
+ function measure$1(label, t0) {
3953
+ const duration = performance.now() - t0;
3954
+ if (s.enabled && s.current) {
3955
+ s.current.entries.push({ label, t: t0 - s.current.startedAt, duration });
3956
+ }
3957
+ return duration;
3958
+ }
3959
+ function getReport() {
3960
+ return {
3961
+ sessions: s.sessions,
3962
+ latest: s.sessions[0] ?? null,
3963
+ };
3964
+ }
3965
+ function clear$1() {
3966
+ s.current = null;
3967
+ s.sessions = [];
3968
+ if (typeof window !== 'undefined')
3969
+ delete window.__gemxPerf;
3970
+ }
3971
+ function enable$1() {
3972
+ s.enabled = true;
3973
+ }
3974
+ function disable$1() {
3975
+ s.enabled = false;
3976
+ }
3977
+ // ── Internal ──────────────────────────────────────────────────────────────────
3978
+ function flush() {
3979
+ if (typeof window === 'undefined')
3980
+ return;
3981
+ window.__gemxPerf = getReport();
3982
+ }
3983
+ // ── Singleton export ──────────────────────────────────────────────────────────
3984
+ const perf = { startSession, endSession, mark: mark$1, measure: measure$1, getReport, clear: clear$1, enable: enable$1, disable: disable$1 };
3985
+
3907
3986
  /**
3908
3987
  * DOM observation setup — ResizeObserver + MutationObserver.
3909
3988
  * Returns a cleanup function that disconnects both observers.
@@ -4644,15 +4723,20 @@ const HEIGHT_RELATED_PROPERTIES = ['height', 'min-height', 'max-height', 'top',
4644
4723
  let elementsWithViewportUnits = new Set();
4645
4724
  let originalValues = new WeakMap();
4646
4725
  // ─── Regex ────────────────────────────────────────────────────────────────────
4647
- /** Fresh instance every call — avoids shared lastIndex state with the g flag. */
4726
+ /**
4727
+ * Stateless test-only regex (no `g` flag) — safe to share across calls.
4728
+ * Used exclusively for `.test()` checks before doing a full replacement.
4729
+ */
4730
+ const VIEWPORT_RE_TEST = /([-.?\d]+)(vh|svh|lvh|dvh|vw|svw|lvw|dvw)/i;
4731
+ /** Fresh `g`-flagged instance for String.replace() callbacks. */
4648
4732
  function createRegex() {
4649
- return /([-.\\d]+)(vh|svh|lvh|dvh|vw|svw|lvw|dvw)/gi;
4733
+ return /([-.?\d]+)(vh|svh|lvh|dvh|vw|svw|lvw|dvw)/gi;
4650
4734
  }
4651
4735
  // ─── Unit conversion ─────────────────────────────────────────────────────────
4652
4736
  function px(value) {
4653
4737
  return `${value.toFixed(2)}px`;
4654
4738
  }
4655
- function getUnitMap(ctx) {
4739
+ function buildUnitMap(ctx) {
4656
4740
  return {
4657
4741
  vh: ctx.targetHeight,
4658
4742
  svh: ctx.targetHeight,
@@ -4664,15 +4748,15 @@ function getUnitMap(ctx) {
4664
4748
  dvw: ctx.targetWidth,
4665
4749
  };
4666
4750
  }
4667
- function toPx(value, unit, ctx) {
4751
+ function toPx(value, unit, unitMap, targetHeight) {
4668
4752
  const u = unit.toLowerCase();
4669
4753
  if (u === '%')
4670
- return (value / 100) * ctx.targetHeight;
4671
- return (value / 100) * (getUnitMap(ctx)[u] ?? 0);
4754
+ return (value / 100) * targetHeight;
4755
+ return (value / 100) * (unitMap[u] ?? 0);
4672
4756
  }
4673
- function convert(value, unit, ctx) {
4757
+ function convert(value, unit, unitMap, targetHeight) {
4674
4758
  const num = parseFloat(value);
4675
- return isNaN(num) ? value : px(toPx(num, unit, ctx));
4759
+ return isNaN(num) ? value : px(toPx(num, unit, unitMap, targetHeight));
4676
4760
  }
4677
4761
  function isHeightRelated(prop) {
4678
4762
  return HEIGHT_RELATED_PROPERTIES.includes(prop);
@@ -4687,11 +4771,13 @@ function extractProperty(cssText, matchOffset) {
4687
4771
  return m ? m[1].toLowerCase() : '';
4688
4772
  }
4689
4773
  function replaceInText(cssText, ctx) {
4774
+ const unitMap = buildUnitMap(ctx);
4775
+ const { targetHeight } = ctx;
4690
4776
  return cssText.replace(createRegex(), (match, value, unit, offset) => {
4691
4777
  if (unit === '%') {
4692
- return isHeightRelated(extractProperty(cssText, offset)) ? convert(value, unit, ctx) : match;
4778
+ return isHeightRelated(extractProperty(cssText, offset)) ? convert(value, unit, unitMap, targetHeight) : match;
4693
4779
  }
4694
- return convert(value, unit, ctx);
4780
+ return convert(value, unit, unitMap, targetHeight);
4695
4781
  });
4696
4782
  }
4697
4783
  // ─── Element tracking ─────────────────────────────────────────────────────────
@@ -4720,7 +4806,7 @@ function processInlineStyles(ctx) {
4720
4806
  let count = 0;
4721
4807
  ctx.doc.querySelectorAll('[style]').forEach((el) => {
4722
4808
  const style = el.getAttribute('style');
4723
- if (style && createRegex().test(style)) {
4809
+ if (style && VIEWPORT_RE_TEST.test(style)) {
4724
4810
  elementsWithViewportUnits.add(el);
4725
4811
  el.setAttribute('style', replaceInText(style, ctx));
4726
4812
  count++;
@@ -4733,7 +4819,7 @@ function processStyleTags(ctx) {
4733
4819
  let count = 0;
4734
4820
  ctx.doc.querySelectorAll('style').forEach((tag) => {
4735
4821
  const css = tag.textContent || '';
4736
- if (createRegex().test(css)) {
4822
+ if (VIEWPORT_RE_TEST.test(css)) {
4737
4823
  tag.textContent = replaceInText(css, ctx);
4738
4824
  count++;
4739
4825
  }
@@ -4751,7 +4837,7 @@ function processRule(rule, ctx) {
4751
4837
  for (let i = 0; i < style.length; i++) {
4752
4838
  const prop = style[i];
4753
4839
  const value = style.getPropertyValue(prop);
4754
- if (value && createRegex().test(value)) {
4840
+ if (value && VIEWPORT_RE_TEST.test(value)) {
4755
4841
  hasVp = true;
4756
4842
  propOriginals.set(prop, value);
4757
4843
  style.setProperty(prop, replaceInText(value, ctx), style.getPropertyPriority(prop));
@@ -4762,8 +4848,11 @@ function processRule(rule, ctx) {
4762
4848
  trackSelector(cssRule.selectorText, propOriginals, ctx);
4763
4849
  }
4764
4850
  if ('cssRules' in rule) {
4765
- for (const r of Array.from(rule.cssRules || [])) {
4766
- count += processRule(r, ctx);
4851
+ const nested = rule.cssRules;
4852
+ if (nested) {
4853
+ for (let i = 0; i < nested.length; i++) {
4854
+ count += processRule(nested[i], ctx);
4855
+ }
4767
4856
  }
4768
4857
  }
4769
4858
  return count;
@@ -4771,25 +4860,29 @@ function processRule(rule, ctx) {
4771
4860
  /** Processes only inline <style> sheets. Linked sheets are handled by processLinkedStylesheets. */
4772
4861
  function processStylesheets(ctx) {
4773
4862
  let total = 0;
4774
- Array.from(ctx.doc.styleSheets).forEach((sheet) => {
4863
+ const sheets = ctx.doc.styleSheets;
4864
+ for (let i = 0; i < sheets.length; i++) {
4865
+ const sheet = sheets[i];
4775
4866
  if (sheet.href)
4776
- return; // deferred to processLinkedStylesheets
4867
+ continue; // deferred to processLinkedStylesheets
4777
4868
  try {
4778
- for (const rule of Array.from(sheet.cssRules || [])) {
4779
- total += processRule(rule, ctx);
4869
+ const rules = sheet.cssRules;
4870
+ for (let j = 0; j < rules.length; j++) {
4871
+ total += processRule(rules[j], ctx);
4780
4872
  }
4781
4873
  }
4782
4874
  catch (e) {
4783
4875
  logger$1.warn('Cannot read stylesheet (CORS?):', e.message);
4784
4876
  }
4785
- });
4877
+ }
4786
4878
  logger$1.log(`Replaced ${total} rules in inline stylesheets`);
4787
4879
  return total;
4788
4880
  }
4789
4881
  async function processLinkedStylesheets(ctx) {
4790
4882
  const links = ctx.doc.querySelectorAll('link[rel="stylesheet"]');
4791
4883
  let count = 0;
4792
- for (const link of Array.from(links)) {
4884
+ for (let i = 0; i < links.length; i++) {
4885
+ const link = links[i];
4793
4886
  // Skip cross-origin — already in browser CSSOM, handled via processStylesheets
4794
4887
  if (link.href && !link.href.startsWith(ctx.win.location.origin)) {
4795
4888
  logger$1.log('Skipping cross-origin CSS:', link.href);
@@ -4798,7 +4891,7 @@ async function processLinkedStylesheets(ctx) {
4798
4891
  try {
4799
4892
  const res = await fetch(link.href);
4800
4893
  let css = await res.text();
4801
- if (createRegex().test(css)) {
4894
+ if (VIEWPORT_RE_TEST.test(css)) {
4802
4895
  css = replaceInText(css, ctx);
4803
4896
  const style = ctx.doc.createElement('style');
4804
4897
  style.textContent = css;
@@ -5057,35 +5150,52 @@ function configure(debug) {
5057
5150
  }
5058
5151
  async function run$1(ctx, activeGlobal, shopFix) {
5059
5152
  // ── Phase 1: beforeProcess ────────────────────────────────────────────────
5153
+ const t1 = perf.mark('phase1.beforeProcess');
5060
5154
  for (const fix of activeGlobal) {
5061
5155
  if (fix.beforeProcess) {
5062
5156
  logger.log(`[beforeProcess] ${fix.name}`);
5157
+ const t = perf.mark(`phase1.${fix.name}.beforeProcess`);
5063
5158
  await fix.beforeProcess(ctx);
5159
+ perf.measure(`phase1.${fix.name}.beforeProcess`, t);
5064
5160
  }
5065
5161
  }
5066
5162
  if (shopFix?.beforeProcess) {
5067
5163
  logger.log('[beforeProcess] shop');
5164
+ const t = perf.mark('phase1.shop.beforeProcess');
5068
5165
  await shopFix.beforeProcess(ctx);
5166
+ perf.measure('phase1.shop.beforeProcess', t);
5069
5167
  }
5168
+ perf.measure('phase1.beforeProcess', t1);
5070
5169
  // ── Phase 2: process ──────────────────────────────────────────────────────
5170
+ const t2 = perf.mark('phase2.process');
5071
5171
  for (const fix of activeGlobal) {
5072
5172
  if (fix.process) {
5073
5173
  logger.log(`[process] ${fix.name}`);
5174
+ const t = perf.mark(`phase2.${fix.name}.process`);
5074
5175
  await fix.process(ctx);
5176
+ perf.measure(`phase2.${fix.name}.process`, t);
5075
5177
  }
5076
5178
  }
5179
+ perf.measure('phase2.process', t2);
5077
5180
  // ── Phase 3: afterProcess ─────────────────────────────────────────────────
5181
+ const t3 = perf.mark('phase3.afterProcess');
5078
5182
  if (shopFix?.afterProcess) {
5079
5183
  logger.log('[afterProcess] shop');
5184
+ const t = perf.mark('phase3.shop.afterProcess');
5080
5185
  await shopFix.afterProcess(ctx);
5186
+ perf.measure('phase3.shop.afterProcess', t);
5081
5187
  }
5082
5188
  for (const fix of activeGlobal) {
5083
5189
  if (fix.afterProcess) {
5084
5190
  logger.log(`[afterProcess] ${fix.name}`);
5191
+ const t = perf.mark(`phase3.${fix.name}.afterProcess`);
5085
5192
  await fix.afterProcess(ctx);
5193
+ perf.measure(`phase3.${fix.name}.afterProcess`, t);
5086
5194
  }
5087
5195
  }
5196
+ perf.measure('phase3.afterProcess', t3);
5088
5197
  // ── Phase 4: getDimensions ────────────────────────────────────────────────
5198
+ const t4 = perf.mark('phase4.getDimensions');
5089
5199
  return new Promise((resolve) => {
5090
5200
  requestAnimationFrame(() => {
5091
5201
  let dimensions = null;
@@ -5110,6 +5220,7 @@ async function run$1(ctx, activeGlobal, shopFix) {
5110
5220
  dimensions = { height: getFinalHeight(ctx.doc, ctx.win), width: getFinalWidth(ctx.doc) };
5111
5221
  }
5112
5222
  logger.log('Final dimensions:', dimensions);
5223
+ perf.measure('phase4.getDimensions', t4);
5113
5224
  resolve(dimensions);
5114
5225
  });
5115
5226
  });
@@ -5237,10 +5348,14 @@ async function run(s) {
5237
5348
  const activeGlobal = getActiveFixes(ctx);
5238
5349
  if (activeGlobal.length > 0)
5239
5350
  s.logger.log(`Active global fixes: ${activeGlobal.map((f) => f.name).join(', ')}`);
5351
+ const tRun = perf.mark('viewport.run');
5240
5352
  try {
5241
- return await run$1(ctx, activeGlobal, s.shopFix);
5353
+ const result = await run$1(ctx, activeGlobal, s.shopFix);
5354
+ perf.measure('viewport.run', tRun);
5355
+ return result;
5242
5356
  }
5243
5357
  catch (err) {
5358
+ perf.measure('viewport.run', tRun);
5244
5359
  s.logger.error('Critical error:', err);
5245
5360
  return { height: s.doc.body?.scrollHeight || 1000, width: s.doc.body?.scrollWidth || 1000 };
5246
5361
  }
@@ -5289,16 +5404,23 @@ async function process(s) {
5289
5404
  s.config.onError?.(new Error('Cannot access iframe document'));
5290
5405
  return;
5291
5406
  }
5407
+ const sessionId = `render-${Date.now()}`;
5408
+ perf.startSession(sessionId);
5409
+ const t0 = perf.mark('orchestrator.process');
5292
5410
  try {
5293
5411
  s.logger.log('Processing viewport units...');
5294
5412
  s.viewportReplacer.start(s.iframe, s.config);
5295
5413
  s.navigationBlocker.start(s.iframe, { debug: s.config.debug });
5296
5414
  const result = await s.viewportReplacer.run();
5415
+ perf.measure('orchestrator.process', t0);
5416
+ perf.endSession();
5297
5417
  s.logger.log('Process completed:', result);
5298
5418
  s.config.onSuccess?.(result);
5299
5419
  dispatchDimensionsEvent(result);
5300
5420
  }
5301
5421
  catch (error) {
5422
+ perf.measure('orchestrator.process', t0);
5423
+ perf.endSession();
5302
5424
  s.logger.error('Failed to process:', error);
5303
5425
  s.config.onError?.(error);
5304
5426
  }
@@ -5792,6 +5914,17 @@ class GXVisualizer extends Visualizer {
5792
5914
  };
5793
5915
  }
5794
5916
 
5917
+ // ── Performance timing ────────────────────────────────────────────────────────
5918
+ function mark(label) {
5919
+ const t = performance.now();
5920
+ console.log(`[Render] ⏱ ${label}`);
5921
+ return t;
5922
+ }
5923
+ function measure(label, startMs) {
5924
+ const ms = (performance.now() - startMs).toFixed(1);
5925
+ console.log(`[Render] ✅ ${label} — ${ms}ms`);
5926
+ }
5927
+ // ── Hook ──────────────────────────────────────────────────────────────────────
5795
5928
  const useHeatmapRender = () => {
5796
5929
  const viewId = useViewIdContext();
5797
5930
  const data = useHeatmapDataContext((s) => s.data);
@@ -5809,6 +5942,7 @@ const useHeatmapRender = () => {
5809
5942
  return;
5810
5943
  if (!payloads || payloads.length === 0)
5811
5944
  return;
5945
+ const t0 = mark('renderHeatmap start');
5812
5946
  const visualizer = vizRef ?? new GXVisualizer();
5813
5947
  if (!vizRef)
5814
5948
  setVizRef(visualizer);
@@ -5816,12 +5950,15 @@ const useHeatmapRender = () => {
5816
5950
  const iframe = iframeRef.current;
5817
5951
  if (!iframe?.contentWindow)
5818
5952
  return;
5953
+ const tHtml = mark('visualizer.html start');
5819
5954
  await visualizer.html(payloads, iframe.contentWindow, viewId);
5955
+ measure('visualizer.html', tHtml);
5820
5956
  startIframe({
5821
5957
  helperRef,
5822
5958
  iframe,
5823
5959
  deviceType,
5824
5960
  size: { width: contentWidth, height: wrapperHeight },
5961
+ t0,
5825
5962
  onSuccess: (height) => {
5826
5963
  if (height)
5827
5964
  setIframeHeight(height);
@@ -5849,12 +5986,14 @@ const useHeatmapRender = () => {
5849
5986
  iframeRef,
5850
5987
  };
5851
5988
  };
5852
- function startIframe({ helperRef, iframe, deviceType = EDeviceType.Desktop, size, onSuccess }) {
5989
+ // ── Helpers ───────────────────────────────────────────────────────────────────
5990
+ function startIframe({ helperRef, iframe, deviceType = EDeviceType.Desktop, size, t0, onSuccess }) {
5853
5991
  const docWidth = size.width ?? 0;
5854
5992
  const docHeight = size.height ?? 0;
5855
5993
  if (docHeight === 0)
5856
5994
  return;
5857
5995
  helperRef.current?.stop();
5996
+ const tHelper = mark('IframeHelper.start');
5858
5997
  const helper = createIframeHelper();
5859
5998
  helperRef.current = helper;
5860
5999
  helper.start({
@@ -5864,6 +6003,8 @@ function startIframe({ helperRef, iframe, deviceType = EDeviceType.Desktop, size
5864
6003
  iframe,
5865
6004
  debug: true,
5866
6005
  onSuccess: (data) => {
6006
+ measure('IframeHelper processing', tHelper);
6007
+ measure('Total render', t0);
5867
6008
  iframe.style.height = `${data.height}px`;
5868
6009
  onSuccess(data.height);
5869
6010
  },