This code is used for pages opened in iframes to precisely adjust the parent iframe height.
When using vh units inside an iframe, element heights will always equal the viewport height of the iframe.
- Your script detects content height changes β sends
postMessageto parent page to update iframe height - Parent page adjusts iframe
height - Iframe viewport height (the calculation basis for
100vh) changes accordingly - Iframe internal layout recalculates
- Content height changes
- Triggers script update again β infinite loop
The following function solves the infinite loop problem:
function ifmHighlyAdaptive() {
const THROTTLE_INTERVAL = 500; // Adjustable as needed
const IFRAME_OFFSET = 12; // Height correction for iframe to avoid scrollbars
let lastHeight = 0;
let resizeObserver = null;
let mutationObserver = null;
let isMonitoringEnabled = true; // Whether height change monitoring is enabled
let isFirstUpdate = true; // Flag to mark if this is the first update
let fixedMinHeight = null; // Record the minHeight set by script (for syncing down when content shrinks)
/**
* Get actual content height of the document
*
* The height of document.body or document.documentElement inside an iframe
* may be constrained by the parent iframe height, no longer reflecting the
* actual content height.
*
* Solution:
* 1. Wrap content in a fixed container (wrapper) inside the iframe
* 2. Only monitor the height of this wrapper
* 3. Use ResizeObserver + MutationObserver + image load listeners
* 4. Don't rely on body/html scrollHeight, only rely on wrapper scrollHeight
*/
function getActualContentHeight() {
const wrapper = document.getElementById("iframe-content-wrapper");
if (!wrapper) return 0;
// The wrapper may have a large minHeight set, which prevents getting
// the real height when content shrinks. Temporarily remove inline
// minHeight to measure "actual content height".
const prevInlineMinHeight = wrapper.style.minHeight;
if (prevInlineMinHeight) {
wrapper.style.minHeight = '';
}
const realContentHeight = wrapper.scrollHeight || wrapper.offsetHeight || 0;
// If a fixed minHeight was previously set by script, and the current
// content height (plus offset) is smaller than this fixed value,
// it means content has shrunk. We need to sync down the minHeight
// to prevent the iframe from being unable to shrink.
if (fixedMinHeight !== null) {
const targetHeightWithOffset = Math.ceil(realContentHeight) + IFRAME_OFFSET;
if (targetHeightWithOffset < fixedMinHeight) {
fixedMinHeight = targetHeightWithOffset;
wrapper.style.minHeight = fixedMinHeight + 'px';
} else {
// Restore previous inline minHeight (if any), otherwise keep current fixed value
wrapper.style.minHeight = prevInlineMinHeight || (fixedMinHeight + 'px');
}
} else {
// When no script-set minHeight exists, restore original inline minHeight
wrapper.style.minHeight = prevInlineMinHeight;
}
return realContentHeight;
}
/**
* Throttle function (or combination of "throttle + last call debounce")
*/
function throttle(fn, wait) {
let lastCall = 0;
let timer = null;
let lastInvokeArgs = null;
const invoke = () => {
timer = null;
lastCall = Date.now();
fn();
};
const throttled = () => {
const now = Date.now();
const remaining = wait - (now - lastCall);
if (remaining <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
invoke();
} else if (!timer) {
timer = window.setTimeout(invoke, remaining);
}
};
throttled.flush = () => {
if (timer) {
clearTimeout(timer);
invoke();
}
};
return throttled;
}
const scheduleUpdate = throttle(updateParentHeight, THROTTLE_INTERVAL);
function updateParentHeight() {
// If monitoring is disabled, don't update height
if (!isMonitoringEnabled) {
return;
}
const height = Math.ceil(getActualContentHeight()) + IFRAME_OFFSET;
// Force update on first update, check height change on subsequent updates
if (!isFirstUpdate && height === lastHeight) return; // Key: prevents infinite loop
console.log('ππ»ππ»ππ»ππ»ππ»ππ»ππ»ππ»ππ»2222222222 [iframe-autosize] Initializing height setting!');
lastHeight = height;
isFirstUpdate = false; // After first update, mark as false
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: "iframeHeightUpdate", height }, "*");
}
}
function startMonitoring() {
// If already monitoring, stop first
stopMonitoring();
isMonitoringEnabled = true;
// ResizeObserver monitors html and body
if (typeof ResizeObserver !== "undefined") {
resizeObserver = new ResizeObserver(scheduleUpdate);
resizeObserver.observe(document.documentElement);
resizeObserver.observe(document.body);
}
// MutationObserver monitors DOM changes
if (typeof MutationObserver !== "undefined") {
mutationObserver = new MutationObserver(scheduleUpdate);
mutationObserver.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
characterData: true
});
}
}
function stopMonitoring() {
isMonitoringEnabled = false;
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
if (mutationObserver) {
mutationObserver.disconnect();
mutationObserver = null;
}
}
function init() {
console.log('ππ»ππ»ππ»ππ»ππ»ππ»ππ»ππ»ππ» [iframe-autosize] Initializing!');
isFirstUpdate = true; // Reset to first update on initialization
startMonitoring();
updateParentHeight(); // Immediately sync on initialization
}
function ready(callback) {
if (document.readyState != 'loading') callback();
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', callback);
else document.attachEvent('onreadystatechange', function () {
if (document.readyState == 'complete') callback();
});
}
window.addEventListener("load", () => {
scheduleUpdate();
scheduleUpdate.flush(); // Ensure final height after load completes
});
function tryParse(str) {
try {
return JSON.parse(str);
} catch (e) {
return null;
}
}
window.addEventListener('message', function (e) {
const data = (typeof e.data === 'string') ? tryParse(e.data) : e.data;
if (!data || data.__iframeResizer !== true) return;
if (data.type === 'parentViewport') {
// Handle parent page viewport changes
ready(function () {
init();
});
} else if (data.type === 'parentChangedHeight') {
// Parent page is changing height, pause monitoring
console.log('βββββββββββ [iframe-autosize] Received pause monitoring message, stopping height monitoring');
stopMonitoring();
} else if (data.type === 'parentChangedHeightComplete') {
// Parent page height change complete, resume monitoring
console.log('πͺπͺπͺπͺπͺπͺπͺπͺπͺπͺπͺ [iframe-autosize] Received resume monitoring message, resuming height monitoring');
// Ensure Observer is completely disconnected
stopMonitoring();
// Use requestAnimationFrame to ensure DOM operations execute in next frame,
// at which point Observer is completely disconnected
requestAnimationFrame(() => {
// Get current iframe actual height and set iframe-content-wrapper to fixed height
// This avoids content height changes caused by vh recalculation, preventing infinite loop
const wrapper = document.getElementById("iframe-content-wrapper");
if (wrapper) {
// Get current iframe actual height (from parent page or use current viewport height)
const currentIframeHeight = data.height;
// Set wrapper height to fixed value to avoid infinite loop from vh changes;
// Also record this fixed height for comparing and reducing minHeight when content shrinks later
if (currentIframeHeight > IFRAME_OFFSET) {
fixedMinHeight = currentIframeHeight;
wrapper.style.minHeight = fixedMinHeight + 'px';
console.log('πͺπͺπͺπͺπͺπͺπͺπͺπͺπͺπͺ [iframe-autosize] Set wrapper height to:', currentIframeHeight);
}
}
// Wait one more frame to ensure minHeight is set before resuming monitoring
requestAnimationFrame(() => {
// Update lastHeight to avoid immediate update trigger
lastHeight = Math.ceil(getActualContentHeight()) + IFRAME_OFFSET;
// Resume monitoring
startMonitoring();
// Update height once immediately after resuming
setTimeout(() => {
updateParentHeight();
}, 100);
});
});
}
});
}import React, { useEffect, useRef, useState } from "react";
type ChildProgramLoaderProps = {
url: string
};
const IframeLoader = (props: ChildProgramLoaderProps) => {
const {
url
} = props;
const IFRAME_OFFSET = 12; // Height correction for iframe to avoid scrollbars
const iframeRef = useRef<HTMLIFrameElement>(null);
const lastHeightRef = useRef<number>(0);
const isFirstHeightUpdateRef = useRef<boolean>(true); // Ensure fast height update on first entry
const timerIdRef = useRef<any>(null);
const [iframeUrl, setIframeUrl] = useState<string | null>(null);
function postMessageToIframe(message: { type: string; height?: number }) {
const iframe = iframeRef.current;
if (!iframe || !iframe.contentWindow) return;
// Send message to child page
iframe.contentWindow.postMessage({
...message,
__iframeResizer: true
}, "*");
}
useEffect(() => {
// Initialize iframe height
if (iframeRef.current) {
iframeRef.current.style.height = '100vh';
}
// Debounce: only write lastHeight back to iframe height after 1 second of silence
const debounceSetHeight = () => {
const iframe = iframeRef.current;
if (!iframe) return;
console.log('π§π§π§π§π§π§π§π§2222222π§π§π§π§π§π§π§π§', lastHeightRef.current)
// If timer is already running, clear it
if (timerIdRef.current) {
clearTimeout(timerIdRef.current);
} else {
// Only send pause monitoring message on first trigger
// Tell child page that parent is changing height, pause monitoring
postMessageToIframe({
type: "parentChangedHeight",
height: lastHeightRef.current
});
console.log('π§π§π§π§π§π§π§π§ [iframe-autosize] Sending pause monitoring message');
}
const delay = isFirstHeightUpdateRef.current ? 120 : 1000;
timerIdRef.current = setTimeout(() => {
const iframeLatest = iframeRef.current;
// Optional: limit height range
const h = lastHeightRef.current;
if (iframeLatest && iframeLatest.style.height !== `${h}px` && h > IFRAME_OFFSET) {
iframeLatest.style.height = `${h + IFRAME_OFFSET}px`;
console.log('π§π§π§π§π§π§π§π§ [iframe-autosize] Height updated to:', h);
}
// After debounceSetHeight completes, tell child page it can resume monitoring
postMessageToIframe({
type: "parentChangedHeightComplete",
height: lastHeightRef.current
});
console.log('π§π§π§π§π§π§π§π§ [iframe-autosize] Sending resume monitoring message');
timerIdRef.current = null;
if (isFirstHeightUpdateRef.current) {
isFirstHeightUpdateRef.current = false;
}
}, delay);
};
// Prepare listener for auto-height messages
function handleMessage(event: MessageEvent) {
const data: any = event.data;
// Handle iframeHeightUpdate message type
if (data?.type === 'iframeHeightUpdate') {
const iframe = iframeRef.current;
if (iframe) {
const heightNum = typeof data.height === 'number' ? data.height : parseInt(data.height, 10);
console.log('π§π§π§π§π§π§π§π§111111π§π§π§π§π§π§π§π§', heightNum)
if (!Number.isNaN(heightNum) && heightNum > 0) {
// Update recorded value and start/refresh debounce
lastHeightRef.current = heightNum;
debounceSetHeight();
}
}
return;
}
}
window.addEventListener('message', handleMessage);
// Set iframe URL to trigger rendering
const _url = url;
setIframeUrl(_url);
return () => {
if (timerIdRef.current) clearTimeout(timerIdRef.current);
window.removeEventListener('message', handleMessage);
// Clear iframe URL to completely unmount iframe
setIframeUrl(null);
};
}, [url]);
// Return null if no iframe should be rendered, this completely unmounts the iframe
if (!iframeUrl) {
return null;
}
return (
<iframe
ref={iframeRef}
src={iframeUrl}
style={{
width: '100%',
height: '100vh',
border: '0',
display: 'block'
}}
onLoad={() => {
// After iframe loads, send parentViewport message to ensure bidirectional constraints
// Also send parentViewport on initialization to ensure bidirectional constraints take effect from the start
// Delay sending to ensure iframe content is loaded
setTimeout(() => {
postMessageToIframe({
type: "parentViewport",
height: window.innerHeight
});
}, 500);
}}
/>
);
}
export default IframeLoader;