Script Order & $(Window).load + Lazy Loading Images

Ryuzaki

お前はもう死んでいる
Moderator
BuSo Pro
Digital Strategist
Joined
Sep 3, 2014
Messages
6,244
Likes
13,130
Degree
9
I have a fun riddle for you guys.

The Background
In the past, I created a comparison table. It has 3 columns and 4 rows. The cells in each row load and end up having different heights due to the amount of text and the images having different heights. So I used $(window).load to fire a function that measures the heights of the cells in each row, finds the tallest one, and sets them all to be the height of the tallest one. This made the table pretty. I'd also run it any time the window was resized after 100ms to make sure it was always pretty.

All was well and good. Now I'm converting all my cool custom field stuff into Gutenberg Blocks, which means this comparison table is now within the_content(). That means it's subject to Lazy Loading (and I can't tell it to skip over certain images with classes or I would. I'm using this one library due to how lightweight it is.

The Mystery
While testing the new custom comparison table block, I noticed on maybe 1 of every 5 loads that the height of rows with images was getting messed up and being very tiny. This let me know that the height was being calculated before the real image was loaded. It was measuring while the 1px by 1px transparent image was still in there.

The scripts were loading in this order:
  1. table.js
  2. lazy-load.js
But remember that I have the function not firing until all scripts were loaded by using $(window).load.

I started playing around and swapped the order of the scripts to be:
  1. lazy-load.js
  2. table.js
And this appears (but I can't be sure) to have solved the issue.

The Questions
This led me to wondering several things. We know that $(window).load waits for all scripts and images and everything to be loaded before firing. So am I correct in thinking that the order of the scripts still mattered? So even though we're waiting for it all to be loaded, it's going to be parsed and fired off in the order it appears in the DOM, right?

Think about this. If $(window).load is waiting for all images to load, does that include or exclude the big images that are being swapped in by the lazy-loading script, assuming that the lazy-loading script fires before the table.js script?

It seems like "everything is loaded" would exclude the big images to be swapped in. Does that mean I'm getting lucky on the timing and that someone on a tablet might see the table all messed up?

My timeline could be:
  1. Everything is loaded
  2. lazy-load.js fires
  3. Because I'm on a fast connection, the new big images are swapped in before step 4
  4. table.js does the height measurement and makes the table pretty
And could someone on a tablet end up having #3 and #4 switched around where the table.js script fires before the big images are swapped in?

Final Question
If my assumption is right and I'm getting lucky on the timing, what can I do to ensure the height calculation only runs after the images are loaded?

I could run a couple delays. I could run the calculation at $(window).load, then 500ms later, then 3 seconds later (or ever later to accommodate tablet users).

Otherwise I'm kind of at a loss for a simple solution without digging into a ridiculous amount of event detection, which seems like overkill here.
 
Here's 3 entirely CSS-based options that might work.

Flexbox
I'd ditch the JS and just go straight CSS flexbox for this. JS, jQuery, and the event loop are like infinitely recursive pain, all the way down, but I am a pessimist. :wink:

Now that's not to say that flexbox is "easy", per se, as it has its own idiosyncrasies and learning curve. That said, it does have some sensible defaults that sound like they'd help. For example, align-items: stretch, which will make items on a row the same height.

Depending on what Flexbox properties you might use, a minor polyfill can get you flexbox browser support back to IE10. Here's a solid guide to flexbox, if you're interested.

CSS Grid
Also, depending on the exact content within the cells, if it is more complex, CSS Grid might be an option. It's a joy to work with, and you can build some truly crazy layouts with it. Once you get the hang of the syntax, it's faster to build complex layouts than any, single, other method I've found. If you want to have your mind blown, check out some of the entries from this contest from Smashing Magazine a couple years ago: CSS Grid Challenge

The downside is, CSS grid browser support isn't quite as great. You can get partial support back to IE10 with some polyfills. There's not quite as good of support for much older Firefox, Chrome, or other old browser versions. That may not be an issue, depending on your audience, but figured I'd mention it.

CSS Tables
For anyone else coming across this thread, and dealing with the issue of tables, I wanted to mention a third alternative. Should you have fairly simple data you need to fit in a table, you might also consider using "CSS tables". That is, the display: table property. This is NOT HTML tables, which most people will think of.

The benefit is, display: table browser support has you covered back to IE8. That way your UX will still rock for those old curmudgeons browsing your list posts on their ancient 486 computers. LOL Also, this property is more flexible than HTML tables.
 
In contrast, I wanted to mention something on the JS side of things. When dealing with things like event tracking, I've really taken to liking the "event delegation" style (aka "event bubbling", aka event propagation). The difference is, you set an event on a parent element and catch the events that "bubble" up to that parent element.

The benefit this can have is, instead of adding and removing event listeners at a lower level, it can help potentially avoid events missing dynamically-loaded content.

telTK.png

This method is even robust enough that it can handle dynamic sites like single page apps (SPAs), still catching drastic content changes. On top of that, it's vanilla JS, so browser compatibility can be less of an issue.

For example, I like to just set my event tracking on the actual document and catch them as they bubble up. One caveat to watch out for, however, is there are several JavaScript methods that can interfere with these events, and potentially even stop them in their tracks.
  • Event.stopPropagation()
    • As the name implies, it stops events from bubbling up completely.
  • return false; in jQuery
    • Under the hood, basically runs both preventDefault() and stopPropagation(), killing your events cold.

This is why I've seriously gotten tired of jQuery-based plugins, and try to strip them out whenever I can. Tons of third party plugins misuse stopPropagation() and/or return false; in ways that really hamper tracking and manipulation outside of the library.

Here's an example of the event delegation style in practice. This isn't quite relevant to what you're asking, Ryuzaki, but hopefully seeing the flow of it at least helps, if you end up needing to rewrite some JS. This is a snippet of event tracking for Google Analytics:

JavaScript:
document.addEventListener('click', function (action) {
  'use strict';

  // Capture element or closest parent
  var featuredImageLink = targetMatch('.entry-image-link');

  // Google Analytics event function. Takes 1 parameter:
  // [1] Event Label
  function gaEvent(label) {
    return ga('send', 'event', 'Category Pages', 'Featured Image', label);
  }

  // Target matches closest parent
  function targetMatch(elem) {
    return action.target.closest(elem);
  }

  // Get element href
  function elemHref(elem) {
    return elem.getAttribute('href');
  }

  // Conditionally fire events with appropriate labels
  if (featuredImageLink) {
    return gaEvent(elemHref(featuredImageLink));
  }

}, false);

So, that starts off putting an event listener on the actual document, in effect, capturing everything. I add 'use strict'; because there's no downside to using it, and can help protect against certain errors in writing bad JS.

The variable, featuredImageLink, is using a function that uses target.closest(). That variable is set to watch featured images on a category page post grid. The reason I'm using target.closest() is, for some elements like say an <a> tag that has spans inside it, sometimes you can run into issues. In cases like that, sometimes trying to get the element's text or other attribute might fail.

The gaEvent() function takes one parameter, which we'll use later. This is the actual event that will end up being sent to Google Analytics, when we call it later.

There's a simple elemHref() function that takes one parameter (the item we're targeting), and tries to get the href attribute from it. In this case, we're simply trying to get the href of the post a user is clicking on, when they click the post's featured image.

Almost done, we have a conditional to only fire gaEvent(), when our targeted item is clicked, and passing that item through as the parameter. In this case, we're passing the target through elemHref() and gaEvent() so we get the right label.

Lastly, the "false" value at the very end is for the "useCapture" parameter of the addEventListener() method. It simply keeps things in event delegation mode (bubbling up so we can catch them). Setting this to true puts the event tracking in "capture" mode, which basically works in the reverse manner.

Anyways, the real reason I brought all of this up, Ryuzaki, is I thought there might be a related issue with what you're dealing with. For one, dealing with the measuring/setting function. Might help to base it on an event listener at the document level, to potentially avoid missing dynamically-loaded content.

Rather than introducing the unpredictability of time as a factor, I might try to look for something related to state on those lazy loaded components, so you can watch for the state to appear. Like imagine if before, there's a boolean attribute that's false, then after the element loads it's true. That state change will bubble up if you listen for it. So rather than guessing on a time window, you could be certain the element is loaded.

Also, the second thing had to do with stopPropagation() or return false; in jQuery. Might be worth checking your libraries for their presence, and trying to figure out if either is interfering with the function you're running.
 
Yeah, I made this table years ago now. I'd rather not re-create it with Flexbox at this point, though I have been exploiting it especially when creating header navigation for desktop. CSS Grid has been my friend for fancy image galleries.

The easiest thing seems to be tracking when the lazy loading swaps images out. I should be able to see when data-src is deleted and src is swapped over to the new URL, then run the calculation.

I ran into all that event.stopPropagation mess in the past when creating a mobile menu that closes when you click off of it. Using stopPropagation is definitely not good. Finding the closest target was the key to testing if you were clicking inside the menu or outside of it, bubbling up the HTML to find the right parent.

I'm still curious though, if events based on $(window).load occur in the order that the scripts are placed in the source code. After sleeping, I'd say the images being swapped in are not a part of that consideration. Either way, seems like state tracking is going to be the solution to ensuring it all happens at the right time, without going back and starting over with Flexbox.
 
Does your lazy-loading function have a callback option? A callback is a function that runs right after whatever function you are running runs.

So ideally: every time lazy-loading loads an a image, a callback function to the table height function is sent to re-adjust tables. You don't have to mess with timestamps or anything. the table adjustment function can run within your load on the initial, then every time a new image is lazy loaded.

More info: JavaScript: What the heck is a Callback?

From my understanding lazy-loading loads when a user is almost near that image's location in the document as they are scrolling, so there can be several scenarios where when scrolling the table isn't the right height - a user has an absurdly fast mouse scroll cause they customized it to be extremely fast (I do).
 
@CCarter, good call on that. I'll check on a callback. I'm pretty sure it doesn't exist but this is as minimal of a script as possible, so I could yank it out of the plugin and add it to my main JS file and add the callback myself.

I wasn't clear about what's happening with the table images. They are above the fold, so as soon as the lazy-loading script fires off it immediately swaps the real images in (because they're already in the viewport).

It has to do with the fact that these images will now be loaded through the_content, when before I had them in the template but outside of the_content, being loaded through custom fields.

I think what I can do is put a unique class on the images, then use that class to watch for the addition of / or removal of a certain attribute. For instance, the real URL to the image is stored in something like data-src and then swapped into src. Then data-src is deleted. So I could watch for the removal of data-src (or something similar, ensuring it only fires off once and at the right time).

The callback is a good idea too, perhaps the best. I'll have to consider the ramifications of incorporating the lazy loading into my own code. Thanks for that idea.
 
Why don't you simply separate your table.js into it's own tiny .js file, make it only load on the pages with the tables, and then make it have the lazy loading script as a dependency, so it loads after the lazy loading script.

I'm very positive that $(window).load is waiting on the scripts to load but not waiting on the images to be swapped from the lazy loading, as that occurs after all the resources are loaded, meaning table.js is firing off first for most connections.

Even using the dependency method, you may run into this problem. Ideally you need a way to make those images not be lazy loaded since they're above the fold anyways. Switching plugins might be a pain, but it's probably the best solution in order to exempt those images.
 
Why don't you simply separate your table.js into it's own tiny .js file, make it only load on the pages with the tables, and then make it have the lazy loading script as a dependency, so it loads after the lazy loading script.

I tried that. You described the problem accurately in your next paragraph. The resources all load (including images), then the script fires like it should, but this happens before the lazy loading occurs.

What I ended up doing was getting away from the plugin I was using. It was great and light, but it had a couple flaws. The main flaw was there was no attribute I could add to skip images from having lazy loading applied. The other flaw was that it filtered through the_content() and the_thumbnail() or whatever on Wordpress, which was goofy and caused me to need to apply the extra "data-src-lazy" attribute and <noscript> manually outside of the the_content(), etc.

This new one I'm using (WP Rocket Lazy Load) is fantastic. Better yet, it doesn't read through the_content() or anything. It just uses Intersection Observer on all images on the page. This saves me from a lot of manual work. If you want to skip images, like I needed to in the comparison table, you can just apply data-no-lazy="1" to the images and you're done.

Rather than having to jump through hoops and detect image swapping or anything else, it was better to just find a method that allows you to exempt certain images.
 
Back