Lazy load Tracking js in iframe only when iframe is in view
New here? Learn about Bountify and follow @bountify to get notified of new bounties! x

We have a 3rd party widget which we load in partner sites to show their audience some of their syndicated content. In this widget we need to load GTM and some pixel tracking but we only want that to fire when the iframe is actually in view. As we understand the end user will just embed our script in their site so we don’t control a lot of external variables but we don’t want to load GA/ GTM until the widget is actually being seen. (It’s often in the footer so we can’t assume page load of the host is a view for us)

Here is our widget code

https://github.com/Wuddrum/news-widget

And here is a demo of it

https://wuddrum.github.io/news-widget/dist/

The goal would be to modify what we have to basically lazy load the tracking till the iframe is in view

can you share the code to be embedded ?, if the code is only an iframe that would be a problem due to same origin policy we can get around that by adding some js on the client side and use postMessage to communicate with the iframe
ahmedengu 1 month ago
Sure - all of the code is above with example but the embed code looks like this https://gist.github.com/quotient/0ff158749e19c0cb41f70d6ee7b17a12
Qdev 1 month ago
thanks, gonna prepare a pull request for you
ahmedengu 1 month ago
awarded to Wuddrum
Tags
javascript

Crowdsource coding tasks.

2 Solutions


Greetings,

You may check my pull request here:
https://github.com/Wuddrum/news-widget/pull/1

kindly feel free to address any issues you may find or and additions, also if you want you can share the trackers code to make them load for you.

the current behavior when you scroll you gonna see a console message says:
onScroll_parent
and a corresponding one from the iframe says:
received: onScroll

you may test here:
https://htmlpreview.github.io/?https://github.com/ahmedengu/news-widget/blob/master/index.html

to add GTM just replace the content on the receiveMessage function with:

(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXX');

the same thing could be done with GA:

  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'GA_MEASUREMENT_ID');

also on the receiveMessage function you may need to check for the event.origin or event.data as a security check or to prevent collisions but am not aware if you are using any other postMessage or what origins you may want to restrict to, however a simple check would be like this:

function receiveMessage(event) {
    if (event.data == "s") {
        console.log("received: " + event.data);
    }
}

the code can be compressed to this form that’s 366 characters, totaling 366 bytes (counting the script only total up to 546) and still supports all browsers:

 <script>!function(t,o,i){t.addEventListener("message",function(e){var n=e.data;n+1<2||(o.getElementById(i).style.height=n+"px")}),t.onload=function(){var e=o.getElementById(i);function n(){t.innerHeight>e.getBoundingClientRect().top&&(o.removeEventListener("scroll",n),e.contentWindow.postMessage("s","*"))}o.addEventListener("scroll",n),n()}}(window,document,"NewsWidget");</script>
<iframe id="NewsWidget" src="//ahmedengu.github.io/news-widget/dist/embed.html?numItems=8&maxItemsPerRow=4" width="100%" frameborder="0" scrolling="no"></iframe>

Best regards,

Ahmed

do you mind making a codepen or similar. this demo link is rendering weird and its difficult to work through whats going on with it?
Qdev 1 month ago
sure, gonna prepare a codepen in a minute
ahmedengu 1 month ago
you may test here https://codepen.io/ahmedengu/project/full/ABLqLz , gonna update my PR too
ahmedengu 1 month ago
you may find the compressed code in the dist folder https://github.com/ahmedengu/news-widget/tree/master/dist , also the full version could be found on the root directory you may use https://jscompress.com to compress the code easily after adding the GA/GTM code
ahmedengu 1 month ago
I'd argue that changing your solution to imitate mine more closely, while omitting a bunch of logic is not the best idea.
What if there's another script or extension that's posting integer messages to the page? You'd now get random widget resizing.
What if the user is refreshing, using browser's back button or is auto-scrolled bellow the widget? Tracking still gets fired, even if the widget is not in viewport.
Same goes for pages that flow horizontally or pages that offload/store contents to sides.
Can you be certain that by assigning window.onload you won't overwrite or get overwritten by another script on the page that also might use it?
Wuddrum 1 month ago
You are totally correct, i apologize for doing that.
ahmedengu 1 month ago
Winning solution

Hey Qdev, here are my 2 options for the solution:

Option A

Demo: https://wuddrum.github.io/news-widget-lazy-a/

Source: https://github.com/Wuddrum/news-widget-lazy-a

Widget code for testing:

<script>window.addEventListener("message",function(a){"wuddrum.github.io"===a.origin.split("://")[1]&&(document.getElementById("NewsWidget").style.height=a.data+"px")});</script>
<iframe id="NewsWidget" src="//wuddrum.github.io/news-widget-lazy-a/embed.html?numItems=8&maxItemsPerRow=4" width="100%" frameborder="0" scrolling="no"></iframe>

This solution is using IntersectionObserver. Because of how it works, all of the implementation (17 lines of code) can reside in embed itself, without the need of increasing the size of widget code.

This is a more reliable and robust solution over Option B.

The major problem with this approach is browser compatibility. The API is currently not available in IE and Safari browsers. As a fallback for those browsers I've made the tracking call to fire on page load. The good news is that Safari has this API implemented in the next version of browsers (both Mac and iOS), so only IE will be left unsupported.

Option B

Demo: https://wuddrum.github.io/news-widget-lazy-b/

Source: https://github.com/Wuddrum/news-widget-lazy-b
Widget code for testing:

<script>!function(c,d,f,g){d("message",function(a){"wuddrum.github.io"===a.origin.split("://")[1]&&(f(g).style.height=a.data+"px")});d("load",function(){var a=f(g),e=function(){var b=a.getBoundingClientRect();0<b.right&&b.left<c.innerWidth&&0<b.bottom&&b.top<c.innerHeight&&(c.removeEventListener("scroll",e),a.contentWindow.postMessage({v:1},"*"))};d("scroll",e);e()})}(window,window.addEventListener.bind(window),document.getElementById.bind(document),"NewsWidget");</script>
<iframe id="NewsWidget" src="//wuddrum.github.io/news-widget-lazy-b/embed.html?numItems=8&maxItemsPerRow=4" width="100%" frameborder="0" scrolling="no"></iframe>

This solution is using the standard getBoundingClientRect(), that's used for these problems, when browser compatibility even with IE is required. Unfortunately this code can't be run from within the embed itself, so the widget size suffers and is not as lean as Option A (161 bytes vs 460 bytes).

Another possible problem with this approach is that if the widget is hosted on some kind of scrolling element, the visibility calculations might start acting unreliably.

Both options work on both - vertically and horizontally flowing pages (or pages that might hide their elements on sides).

Edit: Slimmed down the widgets for both options a bit. A few more bytes can be saved here and there, but it wouldn't make much of a difference.

hey there I am having a tough time testing on the downloaded code. https://crts-assets.s3.amazonaws.com/1851/news-widget-lazy-a-master/index.html seems to be looking like this for me - https://www.dropbox.com/s/m7923vu0kb19moj/2019-03-20_09-36-34.png?dl=0 I get similar issue with option B. Thanks!
Qdev 1 month ago
Hey!
It's happening because widget.js is filtering the messages by their origin (to not accidentaly resize the iframe if the message comes from the wrong origin).
Inside widget.js , on the line that reads if (e.origin.split('://')[1] !== 'wuddrum.github.io') return;, you must change the domain to one, where embed.html is hosted on.
Looks like for this particular case the line should read if (e.origin.split('://')[1] !== 'crts-assets.s3.amazonaws.com') return;
Wuddrum 1 month ago
For testing purposes, if you don't know what domain you might get, or they change often, you can just comment out the line.
Wuddrum 1 month ago
View Timeline