I have an image intensive site built with knockout & includes jQuery.
These are in foreach loops:
<!-- ko foreach: {data: categories, as: 'categories'} -->
<!-- Category code -->
<!-- ko foreach: {data: visibleStations, as: 'stations'} -->
<!-- specific code -->
<img class="thumb lazy" data-bind="attr: { src: imageTmp,
'data-src': imageThumb,
alt: name,
'data-original-title': name },
css: {'now-playing-art': isPlaying}">
<!-- /ko -->
<!-- /ko -->
So basically when I create these elements, imageTmp is a computed observable that returns a temporary url, and imageThumb gets set to a real url from the CDN.
And I also have this chunk of code, call this the Lazy Sweeper:
var lazyInterval = setInterval(function () {
$('.lazy:in-viewport').each(function () {
$(this).attr('src', $(this).data('src')).bind('load', function(){
$(this).removeClass('lazy')
});
});
}, 1000);
That code goes and looks for these images that are in the viewport (using a custom selector to only find images on the screen) and then sets the src
to the data-src
.
The behavior we want is to avoid the overhead of loading a jillion (er, actually, a few hundred) that the user won't see.
The behavior we are seeing is that on first load, it looks like after ko.applyBindings()
is called somehow the Lazy Sweeper gets clobbered and we see the images revert to the default image. Then the sweeper re-runs and we see them display again.
It's not clear to us how best to implement this in a more knockout-ish way.
Thoughts? Insights? Ideas?
I got an answer on twitter mentioning a different lazyloading library. That did not solve the problem - the problem is not understanding how the DOM and the ko representations need to interact to set up lazyloading. I believe what I need is a better way to think about the problem of creating a knockout model that sets imageTmp
, and responds to lazyloading based on whether it's in the viewport, and then updates the model once imageThumb
(the real image) is loaded.
Update: now with a working example.
My approach would be:
load
eventThe viewmodel
add a showPlaceholder
flag which contains our state:
this.showPlaceholder = ko.observable(true);
add a computed observable that always returns the currently correct image url, depending on that state:
this.imageUrl = ko.computed(function() {
return this.showPlaceholder() ? this.imageTemp() : this.imageThumb();
}, this);
Now all we have to do is set showPlaceholder
to false whenever an image should load. More on that in a minute.
The binding
Our bindings job is to set the <img src>
whenever the computed imageUrl
changes. If the src is the real image, it should remove the lazy
class after loading.
ko.bindingHandlers.lazyImage = {
update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var $element = $(element),
// we unwrap our imageUrl to get a subscription to it,
// so we're called when it changes.
// Use ko.utils.unwrapObservable for older versions of Knockout
imageSource = ko.unwrap(valueAccessor());
$element.attr('src', imageSource);
// we don't want to remove the lazy class after the temp image
// has loaded. Set temp-image-name.png to something that identifies
// your real placeholder image
if (imageSource.indexOf('temp-image-name.png') === -1) {
$element.one('load', function() {
$(this).removeClass('lazy');
});
}
}
};
The Lazy Sweeper
All this needs to do is give a hint to our viewmodel that it should switch from placeholder to real image now.
The ko.dataFor(element)
and ko.contextFor(element)
helper functions give us access to whatever is bound against a DOM element from the outside:
var lazyInterval = setInterval(function () {
$('.lazy:in-viewport').each(function () {
if (ko.dataFor(this)) {
ko.dataFor(this).showPlaceholder(false);
}
});
}, 1000);
I am not familiar with Knockout.js so i can't point you in the direction of a more 'knockoutish way', however: So don't consider this a full answer, just a tip to make it less expensive to check every image.
First: You could optimize your code for a bit
var lazyInterval = setInterval(function () {
$('.lazy:in-viewport').each(function () {
$(this)
.attr('src', $(this).data('src'))
.removeClass('lazy');
// you want to remove it from the loadable images once you start loading it
// so it wont be checked again.
// if it won't be loaded the first time,
// it never will since the SRC won't change anymore.
});
}, 1000);
also: if you check for images in your viewport but your viewport does not change, you are just rechecking them over ander over again for no good reason... You could add a 'dirty flag' to check if the viewport actually changed.
var reLazyLoad = true;
var lazyInterval = setInterval(function () {
if (! reLazyLoad) return;
...current code...
reLazyLoad = false;
}, 1000);
$(document).bind('scroll',function(){ reLazyLoad = true; });
And of course, you want it to be rechecked every time you modify the DOM in this case.
This doesn't solve your databinding problem, but it helps on the performance part :-)
(you could also just make the lazySweeper a throttled function and call it everytime something changes (either viewport or dom). Creates prettier code...)
And last : Can't you add the lazy-class using the data-binding? That way, it will only get picked up by the lazySweeper once the binding is complete... (came up with this while typing. I really don't know how knockout js works with databinding so it's a longshot)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With