blob: 1ade18c0698c33f1177c2acf70c384d7eabd5251 [file] [log] [blame]
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Code for lazy loading images on the client side.
* This javascript is part of LazyloadImages filter.
*
* @author nikhilmadan@google.com (Nikhil Madan)
*/
goog.require('pagespeedutils');
// Exporting functions using quoted attributes to prevent js compiler from
// renaming them.
// See http://code.google.com/closure/compiler/docs/api-tutorial3.html#dangers
window['pagespeed'] = window['pagespeed'] || {};
var pagespeed = window['pagespeed'];
/**
* @constructor
* @param {string} blankImageSrc The blank placeholder image used for images
* that are not visible.
*/
pagespeed.LazyloadImages = function(blankImageSrc) {
/**
* Array of images that have been deferred.
* @type {Array.<Element>}
* @private
*/
this.deferred_ = [];
/**
* The number of additional pixels above or below the current viewport
* within which to fetch images. This can be configured for producing a
* smoother experience for the user while scrolling down by fetching images
* just outside the current viewport. Should be a positive number.
* @type {number}
* @private
*/
this.buffer_ = 0;
/**
* Boolean indicating whether we should force loading of all images,
* irrespective of whether or not they are visible. This is set to true when
* we want to load all images after onload is triggered.
* @type {boolean}
* @private
*/
this.force_load_ = false;
/**
* The blank placeholder image used for images that are not visible.
* @type {string}
* @private
*/
this.blank_image_src_ = blankImageSrc;
/**
* The ID of the event that is currently scheduled to be triggered to detect
* and display visible images.
* @private
*/
this.scroll_timer_ = null;
/**
* The time the last scroll event was fired in milliseconds since epoch.
* @type {number}
* @private
*/
this.last_scroll_time_ = 0;
/**
* The minimum time in milliseconds between two scroll events being fired.
* @type {number}
* @private
*/
this.min_scroll_time_ = 200;
/**
* Boolean indicating whether the onload of the page has been triggered.
* @type {boolean}
* @private
*/
this.onload_done_ = false;
/**
* Tracks how many images are left to load before firing the after-onload
* critical image beacon.
* @private {number}
*/
this.imgs_to_load_before_beaconing_ = 0;
};
/**
* Count the number of total imgs that will be lazyloaded.
* @return {number} The number of imgs deferred.
* @private
*/
pagespeed.LazyloadImages.prototype.countDeferredImgs_ = function() {
var deferredImgCount = 0;
var imgs = document.getElementsByTagName('img');
for (var i = 0, img; img = imgs[i]; i++) {
if (img.src.indexOf(this.blank_image_src_) != -1 &&
this.hasAttribute_(img, 'data-pagespeed-lazy-src')) {
deferredImgCount++;
}
}
return deferredImgCount;
};
/**
* Returns a bounding box for the currently visible part of the window.
* @return {{
* top: (number),
* bottom: (number),
* height: (number)
* }}
* @private
*/
pagespeed.LazyloadImages.prototype.viewport_ = function() {
var scrollY = 0;
if (typeof(window.pageYOffset) == 'number') {
scrollY = window.pageYOffset;
} else if (document.body && document.body.scrollTop) {
scrollY = document.body.scrollTop;
} else if (document.documentElement && document.documentElement.scrollTop) {
scrollY = document.documentElement.scrollTop;
}
var height = window.innerHeight || document.documentElement.clientHeight ||
document.body.clientHeight;
return {
top: scrollY,
bottom: scrollY + height,
height: height
};
};
/**
* Returns the position of the top of the given element with respect to the top
* of the page.
* @param {Element} element DOM element to measure offset of.
* @return {number} The position of the element in the page.
* @private
*/
pagespeed.LazyloadImages.prototype.compute_top_ = function(element) {
var position = element.getAttribute('data-pagespeed-lazy-position');
if (position) {
return parseInt(position, 0);
}
position = element.offsetTop;
var parent = element.offsetParent;
if (parent) {
position += this.compute_top_(parent);
}
// Since position is absolute, it should always be >= 0.
position = Math.max(position, 0);
element.setAttribute('data-pagespeed-lazy-position', position);
return position;
};
/**
* Returns a bounding box for the given element.
* @param {Element} element DOM element whose position is to be computed.
* @return {{
* top: (number),
* bottom: (number)
* }}
* @private
*/
pagespeed.LazyloadImages.prototype.offset_ = function(element) {
var top_position = this.compute_top_(element);
return {
top: top_position,
bottom: top_position + element.offsetHeight
};
};
/**
* Returns the value of the given style property for the element.
* @param {Element} element DOM element whose style property is to be computed.
* @param {string} property The CSS property we are trying to find.
* @return {string} The given property if found, and empty string otherwise.
* @private
*/
pagespeed.LazyloadImages.prototype.getStyle_ = function(element, property) {
if (element.currentStyle) {
// IE.
return element.currentStyle[property];
}
if (document.defaultView &&
document.defaultView.getComputedStyle) {
// Other browsers.
var style = document.defaultView.getComputedStyle(element, null);
if (style) {
return style.getPropertyValue(property);
}
}
if (element.style && element.style[property]) {
return element.style[property];
}
// Fallback.
return '';
};
/**
* Returns true if an element is currently visible or within the buffer.
* @param {Element} element The DOM element to check for visibility.
* @return {boolean} True if the element is visible.
* @private
*/
pagespeed.LazyloadImages.prototype.isVisible_ = function(element) {
if (!this.onload_done_ &&
(element.offsetHeight == 0 || element.offsetWidth == 0)) {
// The element is most likely hidden.
// Since we don't know when the element will become visible, we'll try to
// load the image after onload, so that we can improve PLT.
return false;
}
var element_position = this.getStyle_(element, 'position');
if (element_position == 'relative') {
// TODO(ksimbili): Check if this code is still needed. Find out if any other
// alternative will solve this.
// If the element contains a "position: relative" style attribute, assume
// it is visible since getBoundingClientRect() doesn't seem to work
// correctly here.
return true;
}
var viewport = this.viewport_();
var rect = element.getBoundingClientRect();
var top_diff, bottom_diff;
if (rect) {
// getBoundingClientRect() gives the position with respect to the current
// viewport.
top_diff = rect.top - viewport.height;
bottom_diff = rect.bottom;
} else {
var position = this.offset_(element);
top_diff = position.top - viewport.bottom;
bottom_diff = position.bottom - viewport.top;
}
return (top_diff <= this.buffer_ &&
bottom_diff + this.buffer_ >= 0);
};
/**
* Loads the given element if it is visible. Otherwise, adds it to the deferred
* queue. Note that if force_load_ is true, the visibility check is skipped.
* Also sets the onload handler to the image beaconing onload code if required.
* @param {Element} element The element to check for visibility.
*/
pagespeed.LazyloadImages.prototype.loadIfVisibleAndMaybeBeacon =
function(element) {
// Override this element's attributes if they haven't already been overridden.
this.overrideAttributeFunctionsInternal_(element);
var context = this;
window.setTimeout(function() {
var data_src = element.getAttribute('data-pagespeed-lazy-src');
if (data_src) {
if ((context.force_load_ || context.isVisible_(element)) &&
element.src.indexOf(context.blank_image_src_) != -1) {
// Only replace the src if the old value is the one we set. Note that we
// do a 'contains' match to handle the case when the blank src is a url
// starting with //. It is possible that a script has already changed
// the url, in which case, we should not modify it.
// Remove the element from the DOM and add it back in, since simply
// setting the src doesn't seem to always work in chrome.
var parent_node = element.parentNode;
var next_sibling = element.nextSibling;
if (parent_node) {
parent_node.removeChild(element);
}
// Restore the old functions. Make sure that this element actually has
// the old function before restoring it. Otherwise, we'll end up setting
// getAttribute to undefined if we haven't seen this node before.
if (element._getAttribute) {
element.getAttribute = element._getAttribute;
}
// Remove attributes that are no longer needed.
element.removeAttribute('onload');
if (element.tagName && element.tagName == 'IMG') {
// If CriticalImages is defined, we should add the per-image
// checkImageForCriticality logic because the lazyload_images_filter
// would have removed this.
if (pagespeed.CriticalImages) {
pagespeedutils.addHandler(element, 'load', function(e) {
pagespeed.CriticalImages.checkImageForCriticality(this);
if (context.onload_done_) {
context.imgs_to_load_before_beaconing_--;
if (context.imgs_to_load_before_beaconing_ == 0) {
pagespeed.CriticalImages.checkCriticalImages();
}
}
});
}
}
element.removeAttribute('data-pagespeed-lazy-src');
element.removeAttribute('data-pagespeed-lazy-replaced-functions');
// If there was a next sibling, insert element before it.
if (parent_node) {
parent_node.insertBefore(element, next_sibling);
}
// Set srcset before src to avoid potentially loading 2 sizes.
var srcset = element.getAttribute('data-pagespeed-lazy-srcset');
if (srcset) {
element.srcset = srcset;
element.removeAttribute('data-pagespeed-lazy-srcset');
}
// Set the src back to the original.
element.src = data_src;
} else {
context.deferred_.push(element);
}
}
}, 0);
};
pagespeed.LazyloadImages.prototype['loadIfVisibleAndMaybeBeacon'] =
pagespeed.LazyloadImages.prototype.loadIfVisibleAndMaybeBeacon;
/**
* Loads all the images irrespective of whether or not they are in the
* viewport.
*/
pagespeed.LazyloadImages.prototype.loadAllImages = function() {
this.force_load_ = true;
this.loadVisible_();
};
pagespeed.LazyloadImages.prototype['loadAllImages'] =
pagespeed.LazyloadImages.prototype.loadAllImages;
/**
* Loads the visible elements in the deferred queue. Also, removes the loaded
* elements from the deferred queue.
* @private
*/
pagespeed.LazyloadImages.prototype.loadVisible_ = function() {
var old_deferred = this.deferred_;
var len = old_deferred.length;
this.deferred_ = [];
for (var i = 0; i < len; ++i) {
this.loadIfVisibleAndMaybeBeacon(old_deferred[i]);
}
};
/**
* Returns true if the given element has an attribute with the given name.
* @param {Element} element The element whose attributes we are checking.
* @param {string} attribute The attribute we are checking for.
* @return {boolean} True if the element has the given attribute.
* @private
*/
pagespeed.LazyloadImages.prototype.hasAttribute_ =
function(element, attribute) {
if (element.getAttribute_) {
return element.getAttribute_(attribute) != null;
}
return element.getAttribute(attribute) != null;
};
/**
* Overrides attribute functions for all lazily loaded images if they have not
* already been overridden.
*/
pagespeed.LazyloadImages.prototype.overrideAttributeFunctions = function() {
var images = document.getElementsByTagName('img');
for (var i = 0, element; element = images[i]; i++) {
if (this.hasAttribute_(element, 'data-pagespeed-lazy-src')) {
this.overrideAttributeFunctionsInternal_(element);
}
}
};
pagespeed.LazyloadImages.prototype['overrideAttributeFunctions'] =
pagespeed.LazyloadImages.prototype.overrideAttributeFunctions;
/**
* Overrides attribute functions for the given image if they have not already
* been overridden.
* @param {Element} element The element whose attribute functions should be
* overridden.
* @private
*/
pagespeed.LazyloadImages.prototype.overrideAttributeFunctionsInternal_ =
function(element) {
var context = this;
if (!this.hasAttribute_(element, 'data-pagespeed-lazy-replaced-functions')) {
element._getAttribute = element.getAttribute;
element.getAttribute = function(name) {
if (name.toLowerCase() == 'src' &&
context.hasAttribute_(this, 'data-pagespeed-lazy-src')) {
name = 'data-pagespeed-lazy-src';
}
return this._getAttribute(name);
};
element.setAttribute('data-pagespeed-lazy-replaced-functions', '1');
}
};
/**
* Initializes the lazyload module.
* @param {boolean} loadAfterOnload If true, all images will be loaded at
* window.onload.
* @param {string} blankImageSrc The blank placeholder image used for images
* that are not visible.
* is fired. Otherwise, load images on scrolling as they become visible.
*/
pagespeed.lazyLoadInit = function(loadAfterOnload, blankImageSrc) {
var context = new pagespeed.LazyloadImages(blankImageSrc);
pagespeed['lazyLoadImages'] = context;
// Add an event to the onload handler to check if any new images have now
// become visible because of reflows or DOM manipulation. If loadAfterOnload
// is true, load all images on the page.
var lazy_onload = function() {
context.onload_done_ = true;
context.force_load_ = loadAfterOnload;
// Set the buffer to 200 after onload, so that images that are just below
// the fold are pre-loaded and scrolling is smoother.
context.buffer_ = 200;
if (pagespeed.CriticalImages) {
// We don't want to fire the critical image beacon onload check until all
// images have been loaded, so keep a count of the number of images we are
// waiting for. If all images have already been loaded, then go ahead and
// fire the critical image beacon check now.
context.imgs_to_load_before_beaconing_ = context.countDeferredImgs_();
if (context.imgs_to_load_before_beaconing_ == 0) {
pagespeed.CriticalImages.checkCriticalImages();
}
}
context.loadVisible_();
};
pagespeedutils.addHandler(window, 'load', lazy_onload);
// Pre-load the blank image placeholder.
if (blankImageSrc.indexOf('data') != 0) {
new Image().src = blankImageSrc;
}
// Always attach the onscroll, even if the onload option is enabled.
var lazy_onscroll = function() {
if (context.onload_done_ && loadAfterOnload) {
return;
}
// NOTE: We don't delay any scroll event by greater than min_scroll_time_.
if (!context.scroll_timer_) {
// Check that a scroll_timer_ has not been attached already.
var now = new Date().getTime();
var timeout_ms = context.min_scroll_time_;
if (now - context.last_scroll_time_ > context.min_scroll_time_) {
// If the time since the last scroll is greater than min_scroll_time_,
// load visible images immediately.
timeout_ms = 0;
}
// Otherwise, load images after min_scroll_time_.
context.scroll_timer_ = window.setTimeout(function() {
context.last_scroll_time_ = new Date().getTime();
context.loadVisible_();
context.scroll_timer_ = null;
}, timeout_ms);
}
};
pagespeedutils.addHandler(window, 'scroll', lazy_onscroll);
pagespeedutils.addHandler(window, 'resize', lazy_onscroll);
};
pagespeed['lazyLoadInit'] = pagespeed.lazyLoadInit;