From 3eb21f875717055f16cad4f43f5e6f9553d7756a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Sun, 25 Dec 2022 14:31:40 +0100 Subject: [PATCH] Load JS view iframe only once in viewport (#1607) --- assets/js/hooks/js_view.js | 42 +++++++++++++++++++++++++++++++++----- assets/js/lib/utils.js | 4 ++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/assets/js/hooks/js_view.js b/assets/js/hooks/js_view.js index 2c59f34b1..abf3a6fff 100644 --- a/assets/js/hooks/js_view.js +++ b/assets/js/hooks/js_view.js @@ -3,7 +3,12 @@ import { getAttributeOrThrow, parseInteger, } from "../lib/attribute"; -import { isElementHidden, randomId, randomToken } from "../lib/utils"; +import { + isElementHidden, + isElementVisibleInViewport, + randomId, + randomToken, +} from "../lib/utils"; import { globalPubSub } from "../lib/pub_sub"; import { getChannel, @@ -72,7 +77,7 @@ const JSView = { this.channel = getChannel(this.props.sessionId, this.props.clientId); - this.removeIframe = this.createIframe(); + this.iframeActions = this.createIframe(); // Setup child communication this.childReadyPromise = new Promise((resolve, reject) => { @@ -89,7 +94,9 @@ const JSView = { this.hiddenInput.style.display = "none"; this.el.appendChild(this.hiddenInput); - this.loadIframe(); + this.iframeActions.visibilityPromise.then(() => { + this.loadIframe(); + }); // Channel events @@ -154,7 +161,7 @@ const JSView = { destroyed() { window.removeEventListener("message", this._handleWindowMessage); - this.removeIframe(); + this.iframeActions.remove(); this.unsubscribeFromChannelEvents(); this.channel.push("disconnect", { ref: this.props.ref }); @@ -248,11 +255,36 @@ const JSView = { ); }); - return () => { + // We detect when the placeholder enters viewport and becomes visible, + // based on that we can load the iframe contents lazily + + let viewportIntersectionObserver = null; + + const visibilityPromise = new Promise((resolve, reject) => { + if (isElementVisibleInViewport(this.iframePlaceholder)) { + resolve(); + } else { + viewportIntersectionObserver = new IntersectionObserver((entries) => { + if (isElementVisibleInViewport(this.iframePlaceholder)) { + viewportIntersectionObserver.disconnect(); + resolve(); + } + }); + viewportIntersectionObserver.observe(this.iframePlaceholder); + } + }); + + // Cleanup + + const remove = () => { resizeObserver.disconnect(); intersectionObserver.disconnect(); + viewportIntersectionObserver && viewportIntersectionObserver.disconnect(); this.iframe.remove(); + this.iframePlaceholder.remove(); }; + + return { visibilityPromise, remove }; }, repositionIframe() { diff --git a/assets/js/lib/utils.js b/assets/js/lib/utils.js index 90a429623..0ca5cab36 100644 --- a/assets/js/lib/utils.js +++ b/assets/js/lib/utils.js @@ -21,6 +21,10 @@ export function isElementHidden(element) { return element.offsetParent === null; } +export function isElementVisibleInViewport(element) { + return !isElementHidden(element) && isElementInViewport(element); +} + export function clamp(n, x, y) { return Math.min(Math.max(n, x), y); }