FEATURE: Lazily Load Images as they scroll into the viewport.

FEATURE: Lazily Load Images as they scroll into the viewport.

This feature uses the Intersection Observer API

It should be compatible with all modern browsers. Non-Edge IE is NOT supported, so in that particular browser images are loaded by default.

From 6797a710aad582cc3f7aae011c4eb624dedeff70 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Mon, 10 Dec 2018 15:44:38 -0500
Subject: [PATCH] FEATURE: Lazily Load Images as they scroll into the viewport.

This feature uses the Intersection Observer API

https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

It should be compatible with all modern browsers. Non-Edge IE is *NOT*
supported, so in that particular browser images are loaded by default.

diff --git a/app/assets/javascripts/discourse/initializers/post-decorations.js.es6 b/app/assets/javascripts/discourse/initializers/post-decorations.js.es6
index 5e845a6..e9e6921 100644
--- a/app/assets/javascripts/discourse/initializers/post-decorations.js.es6
+++ b/app/assets/javascripts/discourse/initializers/post-decorations.js.es6
@@ -1,5 +1,6 @@
 import highlightSyntax from "discourse/lib/highlight-syntax";
 import lightbox from "discourse/lib/lightbox";
+import { setupLazyLoading } from "discourse/lib/lazy-load-images";
 import { setTextDirections } from "discourse/lib/text-direction";
 import { withPluginApi } from "discourse/lib/plugin-api";
 
@@ -14,6 +15,8 @@ export default {
         api.decorateCooked(setTextDirections);
       }
 
+      setupLazyLoading(api);
+
       api.decorateCooked($elem => {
         const players = $("audio", $elem);
         if (players.length) {
diff --git a/app/assets/javascripts/discourse/lib/lazy-load-images.js.es6 b/app/assets/javascripts/discourse/lib/lazy-load-images.js.es6
new file mode 100644
index 0000000..4f2b772
--- /dev/null
+++ b/app/assets/javascripts/discourse/lib/lazy-load-images.js.es6
@@ -0,0 +1,49 @@
+const OBSERVER_OPTIONS = {
+  rootMargin: "50%" // load images slightly before they're visible
+};
+
+// We hide an image by replacing it with a transparent gif
+function hide(image) {
+  image.classList.add("d-lazyload");
+  image.classList.add("d-lazyload-hidden");
+  image.setAttribute("data-src", image.getAttribute("src"));
+  image.setAttribute(
+    "src",
+    "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+  );
+}
+
+// Restore an image from the `data-src` attribute
+function show(image) {
+  let dataSrc = image.getAttribute("data-src");
+  if (dataSrc) {
+    image.setAttribute("src", dataSrc);
+    image.classList.remove("d-lazyload-hidden");
+  }
+}
+
+export function setupLazyLoading(api) {
+  // Old IE don't support this API
+  if (!("IntersectionObserver" in window)) {
+    return;
+  }
+
+  const observer = new IntersectionObserver(entries => {
+    entries.forEach(entry => {
+      const { target } = entry;
+
+      if (entry.isIntersecting) {
+        show(target);
+        observer.unobserve(target);
+      } else {
+        // The Observer is triggered when entries are added. This allows
+        // us to hide things that start off screen.
+        hide(target);
+      }
+    });
+  }, OBSERVER_OPTIONS);
+
+  api.decorateCooked($post => {
+    $(".lightbox img", $post).each((_, $img) => observer.observe($img));
+  });
+}
diff --git a/app/assets/stylesheets/common/base/lightbox.scss b/app/assets/stylesheets/common/base/lightbox.scss
index db6e3ca..5a9e7ee 100644
--- a/app/assets/stylesheets/common/base/lightbox.scss
+++ b/app/assets/stylesheets/common/base/lightbox.scss
@@ -6,6 +6,16 @@
     opacity: 0.9;
     transition: opacity 0.5s;
   }
+  background: rgba($primary, 0.25);
+}
+
+.d-lazyload-hidden {
+  opacity: 0;
+  box-sizing: border-box;
+}
+
+.cooked img.d-lazyload {
+  transition: opacity 0.4s 0.75s ease;
 }
 
 .lightbox-wrapper {

GitHub

2 Likes

This is so awesome! :confetti_ball: :confetti_ball:

1 Like

One tiny followup here @awesomerobot / @eviltrout is that I notice grey boxes now while just prior to loading, should we just have it totally transparent prior to loading?

1 Like

One thing we could do, but it is probably too fancy / complex, is store an ultra-low-res 1k JPG of the image as a preview. I’ve seen some sites do this… but I’d rather get it shipped as is, I don’t want to gold plate it …

1 Like

My initial thought was that it could be lightened, I’ll make that quick update today. If it’s transparent and for some reason the images take a little while to load, then the page will feel broken.

The fancy blurred version would make this extra good!

2 Likes

The colouring was taken from @hnb-ku 's LazyLoad theme component which was a great inspiration. Please tweak as you see fit @awesomerobot - the CSS should be obvious.

3 Likes