Lazy-loading iframes as they scroll into view I'm working in a Tableau driven system, with large numbers of charts, I needed a way to reduce the strain of the initial load of our app. It's fairly simple, but wanted to ensure I accounted for every scenario, I don't generally utilize lazy loading.

Print isn't a priority.

jsfiddle: Demo (Tableau doesn't work within SO's snippets because of the iframes)

Relevant Javascript

const throttle = (fn, delay) =&gt; {
    let canCall = true;
    return (...args) =&gt; {
      if (canCall) {
        fn.apply(null, args);
        canCall = false;
        setTimeout(() =&gt; {
          canCall = true;
        }, delay);
      }
    };
  };

  const setAttributes = (el, options) =&gt;
    Object.keys(options).forEach(attr =&gt; el.setAttribute(attr, options[attr]));

  const w = window;
  const d = document;
  const b = d.body;
  const x = w.innerWidth || e.clientWidth || b.clientWidth;
  const y = w.innerHeight || e.clientHeight || b.clientHeight;

  /* unecessary, but to have same object format, e.g. readability */
  const win = Object.create(null);
  win.y = y;

  const frame = d.querySelectorAll("iframe");

  /** may be over doing it here... test results */
  const preloadPath = () =&gt;
    frame.forEach(item =&gt; {
      const source = item.getAttribute("data-src");
      const preloadLink = d.createElement("link");
      const head = d.getElementsByTagName("head")[0];

      setAttributes(preloadLink, {
        rel: "preload",
        as: "document",
        type: "text/html",
        crossorigin: "anonymous",
        href: source
      });

      head.insertBefore(preloadLink, head.firstChild);
    });

  const loadFrame = () =&gt;
    frame.forEach(item =&gt; {
      /* only run if not src is present */
      if (!item.src) {
        const source = item.getAttribute("data-src"); // path
        const parent = item.parentElement; // wrapper
        const frameRect = parent.getBoundingClientRect(); // wrapper dimensions

        if (
          w.pageYOffset + win.y &gt;= frameRect.y &amp;&amp; // if frame top exceeds page bottom
          frameRect.y + frameRect.height &gt;= w.pageYOffset // if frame bottom exceeds page top
        ) {
          item.src = source; // set path
          item.removeAttribute("data-src"); // remove placeholder attr
        }
      }
    });

  /** load on docReady */
  d.addEventListener("DOMContentLoaded", [preloadPath, loadFrame]);

  /** throttle on scroll */
  const t = throttle(loadFrame, 64);
  w.addEventListener("scroll", t);
General Feedback

The code in the jsFiddle Demo appears to function well (although it isn't the same as the code above- see the next section for an explanation). There is good usage of const for values that are not re-assigned and functional programming with arrow functions.

Flaw with event listener setup

It appears that the fiddle has different code than appears here, but nonetheless, the code above contains the following line:


d.addEventListener("DOMContentLoaded", [preloadPath, loadFrame]);



The second argument appears to be an array. I haven't seen an array there used before and it doesn't appear to work. The Parameters section of the MDN documentation for addEventListener reads:


  listener
  The object which receives a notification (an object that implements the Event interface) when an event of the specified type occurs. This must be an object implementing the EventListener interface, or a JavaScript function. See The event listener callback for details on the callback itself.1


If you did need to have both functions run when that event occurs, you would either need to have a single callback function that calls both or call addEventListener once for each function. It appears that in the fiddle preloadPath has been removed...

Suggestions

Variable naming

The variable name frame sounds singular, yet it returns a NodeList which would typically contain multiple DOM elements. 


const frame = d.querySelectorAll("iframe");



Thus a more appropriate name would be frames. That way when statements like frames.forEach() is read, it implies that a function is invoked for each of the frames.

The setAttributes() function

Correct me if I am wrong but this function appears to do the same thing that Object.assign(). I was able to replace calls to that function with calls to Object.assign() and still saw the attributes set as expected in Opera and Chrome.

Arrow functions with empty argument lists

You don't have to do this, but _ could be used instead of empty parentheses for arrow functions with no named arguments. Refer to this SO post and its answers for more context.

timeout in function returned by throttle()

While it would only save a couple lines, the arrow function passed to setTimeout() in the function returned by throttle() could be simplified to remove the curly braces. While this would mean that true would be returned, it doesn't affect anything.

const throttle = (fn, delay) =&gt; {
  let canCall = true;
  return (...args) =&gt; {
    if (canCall) {
      fn.apply(null, args);
      canCall = false;
      setTimeout(_ =&gt; canCall = true, delay);
    }
  };
};


1https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameters)