I love the gem render_async because it’s a simple tool to make your controller action thinner.

I was recently considering moving some data into a partial, but realized it was only being used in a single location below the fold.

Behold.

This is an easy way to use render_async, but only rendering the data if it comes into the viewport.

I originally found this solution while optimizing an image-heavy blog that really didn’t need to load images until you scroll the page.

Inside your Rails View: use your standard rails_async code but make sure to be aware of the classes and event name.

<%= render_async(your_path(resource), container_class: 'lazy_async',
                 toggle: { selector: ".lazy_async", event: "load-me"}) do %>
  <div class="text-center p-5">
    <small>Loading stuff...</small>
  </div>
<% end %>

Now notice the javascript uses both the class name and event name.

const lazyClassName = "lazy_async"
const selectorName = "." + lazyClassName

function elementInView(el) {
    el.classList.remove(lazyClassName);
    const event = new Event('load-me');
    el.dispatchEvent(event)
}

document.addEventListener("DOMContentLoaded", function () {
    const lazyLoadContainers = document.querySelectorAll(selectorName);

    if ("IntersectionObserver" in window) {
        const imageObserver = new IntersectionObserver(function (entries, observer) {
            entries.forEach(function (entry) {
                if (entry.isIntersecting) {
                    const target = entry.target;
                    elementInView(target);
                    imageObserver.unobserve(target);
                }
            });
        });

        lazyLoadContainers.forEach(function (target) {
            imageObserver.observe(target);
        });
    } else {
        let lazyloadThrottleTimeout = undefined;

        function lazyload() {
            if (lazyloadThrottleTimeout) {
                clearTimeout(lazyloadThrottleTimeout);
            }

            lazyloadThrottleTimeout = setTimeout(function () {
                const scrollTop = window.scrollY;
                lazyLoadContainers.forEach(function (target) {
                    if (target.offsetTop < window.innerHeight + scrollTop) {
                        elementInView(target);
                    }
                });
                if (lazyLoadContainers.length == 0) {
                    document.removeEventListener("scroll", lazyload);
                    window.removeEventListener("resize", lazyload);
                    window.removeEventListener("orientationChange", lazyload);
                }
            }, 20);
        }

        document.addEventListener("scroll", lazyload);
        window.addEventListener("resize", lazyload);
        window.addEventListener("orientationChange", lazyload);
    }
});

This code is backwards compatible with older browsers and uses setTimeout if the navigator does not have IntersectionObserver.

IntersectionObserver is the industry standard for browser viewport intersection.

Once the observer is intialized, it will trigger on elements with isIntersecting{boolean} to be used however you want.

I like to use a helper method render_async_onscreen


def render_async_onscreen(path, options = {}, &placeholder)
  container_id = "lazy_async_#{SecureRandom.hex(4)}"
  options.reverse_merge!({ container_id: container_id, container_class: 'lazy_async', toggle: { selector: ".lazy_async", event: "load-me" } })
  render_async(path, options, &placeholder)
end