Add sequential image loader with viewport prioritization
- Load property card images 2 at a time instead of all at once - Prioritize images in viewport, then by distance from viewport - Single execution guard prevents concurrent loading runs - Gracefully handles DOM removal (cleared grid aborts pending loads) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
@@ -1735,7 +1735,7 @@
|
|||||||
if (pageData) pageData.state = 'populated';
|
if (pageData) pageData.state = 'populated';
|
||||||
|
|
||||||
if (typeof CardImageLoader !== 'undefined') {
|
if (typeof CardImageLoader !== 'undefined') {
|
||||||
CardImageLoader.loadVisibleImages();
|
CardImageLoader.process();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1810,7 +1810,7 @@
|
|||||||
$page.attr('data-state', 'populated');
|
$page.attr('data-state', 'populated');
|
||||||
|
|
||||||
if (typeof CardImageLoader !== 'undefined') {
|
if (typeof CardImageLoader !== 'undefined') {
|
||||||
CardImageLoader.loadVisibleImages();
|
CardImageLoader.process();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1870,65 +1870,138 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Property Card Image Loader
|
* Sequential Image Loader
|
||||||
* Lazy loads background images with loading spinner
|
* Loads background images 2 at a time, prioritizing viewport
|
||||||
*/
|
*/
|
||||||
var CardImageLoader = {
|
var CardImageLoader = {
|
||||||
|
_isRunning: false,
|
||||||
|
_activeLoads: 0,
|
||||||
|
MAX_PARALLEL: 2,
|
||||||
|
|
||||||
init: function() {
|
init: function() {
|
||||||
this.loadVisibleImages();
|
this.process();
|
||||||
this.bindEvents();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
bindEvents: function() {
|
/**
|
||||||
var self = this;
|
* Start processing images sequentially
|
||||||
|
* Only one execution runs at a time
|
||||||
// Observe DOM changes for AJAX-loaded content
|
*/
|
||||||
if (typeof MutationObserver !== 'undefined') {
|
process: function() {
|
||||||
var observer = new MutationObserver(function(mutations) {
|
if (this._isRunning) return;
|
||||||
self.loadVisibleImages();
|
this._isRunning = true;
|
||||||
});
|
this._activeLoads = 0;
|
||||||
var resultsContainer = document.getElementById('property-results');
|
this._processNext();
|
||||||
if (resultsContainer) {
|
|
||||||
observer.observe(resultsContainer, { childList: true, subtree: true });
|
|
||||||
}
|
|
||||||
var gridContainer = document.getElementById('property-results-grid');
|
|
||||||
if (gridContainer) {
|
|
||||||
observer.observe(gridContainer, { childList: true, subtree: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
loadVisibleImages: function() {
|
/**
|
||||||
var self = this;
|
* Get the next element to load, prioritized by viewport proximity
|
||||||
|
* Returns element in viewport first, then by distance from viewport
|
||||||
|
*/
|
||||||
|
_getNextElement: function() {
|
||||||
|
var $elements = $('.property-card-image[data-bg]');
|
||||||
|
if (!$elements.length) return null;
|
||||||
|
|
||||||
// Load all images in DOM immediately - no viewport check
|
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||||
$('.property-card-image.is-loading[data-bg]').each(function() {
|
var viewportTop = scrollTop;
|
||||||
self.loadImage($(this));
|
var viewportBottom = scrollTop + window.innerHeight;
|
||||||
|
|
||||||
|
var inViewport = [];
|
||||||
|
var outOfViewport = [];
|
||||||
|
|
||||||
|
$elements.each(function() {
|
||||||
|
var rect = this.getBoundingClientRect();
|
||||||
|
var elementTop = rect.top + scrollTop;
|
||||||
|
var elementBottom = elementTop + rect.height;
|
||||||
|
|
||||||
|
// Check if element is in viewport
|
||||||
|
if (elementBottom >= viewportTop && elementTop <= viewportBottom) {
|
||||||
|
// In viewport - distance is 0, but sort by position (top first)
|
||||||
|
inViewport.push({
|
||||||
|
el: this,
|
||||||
|
position: elementTop
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Out of viewport - calculate distance
|
||||||
|
var distance;
|
||||||
|
if (elementTop > viewportBottom) {
|
||||||
|
distance = elementTop - viewportBottom;
|
||||||
|
} else {
|
||||||
|
distance = viewportTop - elementBottom;
|
||||||
|
}
|
||||||
|
outOfViewport.push({
|
||||||
|
el: this,
|
||||||
|
distance: distance
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sort in-viewport by position (top to bottom)
|
||||||
|
inViewport.sort(function(a, b) {
|
||||||
|
return a.position - b.position;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort out-of-viewport by distance (closest first)
|
||||||
|
outOfViewport.sort(function(a, b) {
|
||||||
|
return a.distance - b.distance;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return first in-viewport, or closest out-of-viewport
|
||||||
|
if (inViewport.length) return inViewport[0].el;
|
||||||
|
if (outOfViewport.length) return outOfViewport[0].el;
|
||||||
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
loadImage: function($el) {
|
/**
|
||||||
var bgUrl = $el.data('bg');
|
* Process next image(s) up to MAX_PARALLEL
|
||||||
if (!bgUrl) return;
|
*/
|
||||||
|
_processNext: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
// Remove from loading queue immediately to prevent double-load
|
// Fill up to MAX_PARALLEL slots
|
||||||
$el.removeClass('is-loading').removeAttr('data-bg');
|
while (this._activeLoads < this.MAX_PARALLEL) {
|
||||||
|
var el = this._getNextElement();
|
||||||
|
if (!el) {
|
||||||
|
// No more elements to process
|
||||||
|
if (this._activeLoads === 0) {
|
||||||
|
this._isRunning = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create image to preload
|
var $el = $(el);
|
||||||
var img = new Image();
|
var bgUrl = $el.data('bg');
|
||||||
|
|
||||||
img.onload = function() {
|
// Remove data-bg immediately to prevent double-processing
|
||||||
$el.css('background-image', 'url(' + bgUrl + ')');
|
$el.removeAttr('data-bg').removeClass('is-loading');
|
||||||
$el.addClass('is-loaded');
|
|
||||||
};
|
|
||||||
|
|
||||||
img.onerror = function() {
|
this._activeLoads++;
|
||||||
// On error, show placeholder
|
|
||||||
$el.addClass('is-loaded');
|
|
||||||
$el.removeClass('has-image');
|
|
||||||
};
|
|
||||||
|
|
||||||
img.src = bgUrl;
|
// Use detached Image to preload
|
||||||
|
(function($element, url) {
|
||||||
|
var img = new Image();
|
||||||
|
|
||||||
|
img.onload = function() {
|
||||||
|
// Only set if element still exists in DOM
|
||||||
|
if ($.contains(document, $element[0])) {
|
||||||
|
$element.css('background-image', 'url("' + url + '")');
|
||||||
|
$element.addClass('is-loaded');
|
||||||
|
}
|
||||||
|
self._activeLoads--;
|
||||||
|
self._processNext();
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = function() {
|
||||||
|
// On error, mark as loaded but remove has-image
|
||||||
|
if ($.contains(document, $element[0])) {
|
||||||
|
$element.addClass('is-loaded').removeClass('has-image');
|
||||||
|
}
|
||||||
|
self._activeLoads--;
|
||||||
|
self._processNext();
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = url;
|
||||||
|
})($el, bgUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user