| /* |
| * Copyright 2013 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. |
| */ |
| |
| goog.provide('pagespeed.CriticalImages'); |
| |
| goog.require('goog.array'); |
| goog.require('goog.dom.TagName'); |
| goog.require('pagespeedutils'); |
| |
| |
| |
| /** |
| * Code for detecting and sending to server the critical images (images above |
| * the fold) on the client side. |
| * @param {string} beaconUrl The URL on the server to send the beacon to. |
| * @param {string} htmlUrl URL of the page the beacon is being inserted on. |
| * @param {string} optionsHash The hash of the rewrite options. This is required |
| * to perform the property cache lookup when the beacon is handled by the |
| * sever. |
| * @param {boolean} checkRenderedImageSizes The bool to show if resizing is |
| * being done using the rendered dimensions. If yes we capture the rendered |
| * dimensions and send it back with the beacon. |
| * @param {string} nonce The nonce sent by the server. |
| * @constructor |
| * @private |
| */ |
| pagespeed.CriticalImages.Beacon_ = function( |
| beaconUrl, htmlUrl, optionsHash, checkRenderedImageSizes, nonce) { |
| this.beaconUrl_ = beaconUrl; |
| this.htmlUrl_ = htmlUrl; |
| this.optionsHash_ = optionsHash; |
| this.nonce_ = nonce; |
| /** @private {{height: number, width: number}} */ |
| this.windowSize_ = pagespeedutils.getWindowSize(); |
| this.checkRenderedImageSizes_ = checkRenderedImageSizes; |
| /** @private {!Object.<string, boolean>} */ |
| this.imgLocations_ = {}; |
| |
| // TODO(jud): Consider using goog.structs.Set instead of using an array + map |
| // combination below. |
| /** |
| * Array of critical image URL hash keys. |
| * @private {!Array.<string>} |
| */ |
| this.criticalImages_ = []; |
| /** |
| * Object used to store the keys from this.criticalImages_ so that we get a |
| * unique list of them. |
| * @private {!Object.<string, boolean>} |
| */ |
| this.criticalImagesKeys_ = {}; |
| }; |
| |
| |
| /** |
| * Returns the absolute position of the top left corner of the element. |
| * @param {!Element} element DOM element to calculate the location of. |
| * @return {{top: number, left: number}} |
| * @private |
| */ |
| pagespeed.CriticalImages.Beacon_.prototype.elLocation_ = function(element) { |
| var rect = element.getBoundingClientRect(); |
| |
| // getBoundingClientRect() is w.r.t. the viewport. Add the amount scrolled to |
| // calculate the absolute position of the element. |
| // From https://developer.mozilla.org/en-US/docs/DOM/window.scrollX |
| var body = document.body; |
| var scrollX = 'pageXOffset' in window ? window.pageXOffset : |
| (document.documentElement || body.parentNode || body).scrollLeft; |
| var scrollY = 'pageYOffset' in window ? window.pageYOffset : |
| (document.documentElement || body.parentNode || body).scrollTop; |
| |
| return { |
| top: rect.top + scrollY, |
| left: rect.left + scrollX |
| }; |
| }; |
| |
| |
| /** |
| * Returns true if an element is critical, meaning it is visible upon initial |
| * page load. |
| * @param {!Element} element The DOM element to check for visibility. |
| * @return {boolean} |
| * @private |
| */ |
| pagespeed.CriticalImages.Beacon_.prototype.isCritical_ = function(element) { |
| // TODO(jud): We can perform a more efficient critical image check if lazyload |
| // images is enabled, and this beacon code runs after the lazyload JS has |
| // initially executed. Specifically, we know an image is not critical if it |
| // still has the 'data-pagespeed-lazy-src' attribute, meaning that the image |
| // was not visible in the viewport yet. This will save us potentially many |
| // calls to the expensive getBoundingClientRect(). |
| |
| // Make sure the element is visible first before checking its position on the |
| // page. Note, this check works correctly with the lazyload placeholder image, |
| // since that image is a 1x1 pixel, and styling it display=none also sets |
| // offsetWidth and offsetHeight to 0. |
| if (element.offsetWidth <= 0 && element.offsetHeight <= 0) { |
| return false; |
| } |
| |
| var elLocation = this.elLocation_(element); |
| // Only return one image as critical if there are multiple images that have |
| // the same location. This is to handle sliders with many images in the same |
| // location, but most of which only appear after onload. |
| var elLocationStr = elLocation.top.toString() + ',' + elLocation.left; |
| if (this.imgLocations_.hasOwnProperty(elLocationStr)) { |
| return false; |
| } else { |
| this.imgLocations_[elLocationStr] = true; |
| } |
| |
| return elLocation.top <= this.windowSize_.height && |
| elLocation.left <= this.windowSize_.width; |
| }; |
| |
| |
| /** |
| * Inserts the image key string into criticalImages_ and criticalImagesKeys_ |
| * if it is critical (visible). |
| * @param {!Element} element The DOM element to check for visibility. |
| * @private |
| */ |
| pagespeed.CriticalImages.Beacon_.prototype.insertIfImageIsCritical_ = |
| function(element) { |
| var key = element.getAttribute('data-pagespeed-url-hash'); |
| if (key && !(key in this.criticalImagesKeys_) && |
| this.isCritical_(element)) { |
| this.criticalImages_.push(key); |
| this.criticalImagesKeys_[key] = true; |
| } |
| }; |
| |
| |
| /** |
| * Checks position of image element on onload of the image to decide whether |
| * it is visible or not and adds it to a map if critical. |
| * @param {!Element} element The DOM element to check for visibility. |
| */ |
| pagespeed.CriticalImages.Beacon_.prototype.checkImageForCriticality = |
| function(element) { |
| // TODO(jud): Remove the check for getBoundingClientRect below, either by |
| // making elLocation_ work correctly if it isn't defined, or updating the |
| // user agent whitelist to exclude UAs that don't support it correctly. |
| if (element.getBoundingClientRect) { |
| this.insertIfImageIsCritical_(element); |
| } |
| }; |
| |
| |
| /** |
| * Checks position of image element on onload of the image to decide whether |
| * it is visible or not and adds it to a map if critical. |
| * @param {Element} element The DOM element to check for visibility. |
| * @export |
| */ |
| pagespeed.CriticalImages.checkImageForCriticality = function(element) { |
| pagespeed.CriticalImages.beaconObj_.checkImageForCriticality(element); |
| }; |
| |
| |
| /** |
| * Check all images to see if they are critical and beacon when finished. Should |
| * be called either at page onload, or when the onload handler for all images |
| * has finished running if lazyload_images is enabled. |
| * @export |
| */ |
| pagespeed.CriticalImages.checkCriticalImages = function() { |
| pagespeed.CriticalImages.beaconObj_.checkCriticalImages_(); |
| }; |
| |
| |
| /** |
| * Check position of images and input tags and beacon back all visible images. |
| * This method is triggered on page onload and goes over all image elements |
| * available at this point, and merges this set with the set of visible image |
| * elements detected via image onload logic (via checkImageForCriticality). |
| * Images detected at image-onload time are more accurate in detecting initial |
| * images of slideshow-like features. |
| * @private |
| */ |
| pagespeed.CriticalImages.Beacon_.prototype.checkCriticalImages_ = function() { |
| // Start with a fresh imgLocations_ map so that anything that did not get a |
| // chance to get detected at image-onload time because of accidental overlap |
| // between image locations will now have a chance to get identified. Note that |
| // in case of slideshows, this can cause duplicate images to be detected, but |
| // the correct first slideshow image would already have been detected at image |
| // onload time. |
| this.imgLocations_ = {}; |
| // Generate a list of the elements that can be considered critical. |
| var tags = [goog.dom.TagName.IMG, goog.dom.TagName.INPUT]; |
| var elemsToCheck = []; |
| for (var i = 0; i < tags.length; ++i) { |
| elemsToCheck = elemsToCheck.concat( |
| goog.array.toArray(document.getElementsByTagName(tags[i]))); |
| } |
| |
| // Return early if there aren't any items to check. |
| if (elemsToCheck.length == 0) { return; } |
| |
| // Verify that the browser supports all the features we need, and bail out if |
| // it doesn't. |
| // TODO(jud): Remove the check for getBoundingClientRect, either by making |
| // elLocation_ work correctly if it isn't defined, or updating the user agent |
| // whitelist to exclude UAs that don't support it correctly. |
| if (!elemsToCheck[0].getBoundingClientRect) { return; } |
| |
| for (var i = 0, element; element = elemsToCheck[i]; ++i) { |
| this.insertIfImageIsCritical_(element); |
| } |
| var data = 'oh=' + this.optionsHash_; |
| if (this.nonce_) { |
| data += '&n=' + this.nonce_; |
| } |
| |
| var isDataAvailable = this.criticalImages_.length != 0; |
| if (isDataAvailable) { |
| data += '&ci=' + encodeURIComponent(this.criticalImages_[0]); |
| for (var i = 1; i < this.criticalImages_.length; ++i) { |
| var tmp = ',' + encodeURIComponent(this.criticalImages_[i]); |
| if (data.length + tmp.length <= pagespeedutils.MAX_POST_SIZE) { |
| data += tmp; |
| } |
| } |
| } |
| |
| // Add rendered image dimensions as a query param to the beacon URL. |
| if (this.checkRenderedImageSizes_) { |
| var tmp = '&rd=' + |
| encodeURIComponent(JSON.stringify(this.getImageRenderedMap())); |
| if (data.length + tmp.length <= pagespeedutils.MAX_POST_SIZE) { |
| data += tmp; |
| } |
| isDataAvailable = true; |
| } |
| |
| // Export the URL for testing purposes. |
| pagespeed.CriticalImages.beaconData_ = data; |
| if (isDataAvailable) { |
| // TODO(jud): This beacon should coordinate with the add_instrumentation JS |
| // so that only one beacon request is sent if both filters are enabled. |
| pagespeedutils.sendBeacon(this.beaconUrl_, this.htmlUrl_, data); |
| } |
| }; |
| |
| |
| /** |
| * Retrieves the rendered width and height of images. The returned object uses |
| * abbreviated key names (rw = rendered width, oh = original height, etc.) to |
| * keep the data sent back in the beacon compact. |
| * @return {!Object.<string, { |
| * rw: number, |
| * rh: number, |
| * ow: number, |
| * oh: number}>} Object mapping an image's data-pagespeed-url-hash to its |
| * original and rendered widths and heights. |
| */ |
| pagespeed.CriticalImages.Beacon_.prototype.getImageRenderedMap = function() { |
| var renderedImageDimensions = {}; |
| // TODO(poojatandon): Get elements for 'input' tag with type="image". This |
| // currently doesn't work because input tags don't support naturalWidth and |
| // naturalHeight. |
| var images = document.getElementsByTagName(goog.dom.TagName.IMG); |
| if (images.length == 0) { return {}; } |
| |
| // naturalWidth and naturalHeight is defined for all browsers except in IE |
| // versions 8 and before (non HTML5 support). |
| var img = images[0]; |
| if (!('naturalWidth' in img) || !('naturalHeight' in img)) { return {}; } |
| |
| for (var i = 0; img = images[i]; ++i) { |
| var key = img.getAttribute('data-pagespeed-url-hash'); |
| if (!key) { continue; } |
| if ((!(key in renderedImageDimensions) && |
| img.width > 0 && img.height > 0 && |
| img.naturalWidth > 0 && img.naturalHeight > 0) || |
| ((key in renderedImageDimensions) && |
| img.width >= renderedImageDimensions[key].rw && |
| img.height >= renderedImageDimensions[key].rh)) { |
| renderedImageDimensions[key] = { |
| 'rw' : img.width, |
| 'rh' : img.height, |
| 'ow' : img.naturalWidth, |
| 'oh' : img.naturalHeight |
| }; |
| } |
| } |
| return renderedImageDimensions; |
| }; |
| |
| |
| /** @private string */ |
| pagespeed.CriticalImages.beaconData_ = ''; |
| |
| |
| /** @private Object Beacon object */ |
| pagespeed.CriticalImages.beaconObj_; |
| |
| |
| /** |
| * Gets the data sent in the beacon after pagespeed.CriticalImages.Run() |
| * completes. Used to verify that the beacon ran correctly in tests. |
| * @return {string} |
| * @export |
| */ |
| pagespeed.CriticalImages.getBeaconData = function() { |
| return pagespeed.CriticalImages.beaconData_; |
| }; |
| |
| |
| /** |
| * Scans images and beacons back the visible ones at the onload event. |
| * @param {string} beaconUrl The URL on the server to send the beacon to. |
| * @param {string} htmlUrl URL of the page the beacon is being inserted on. |
| * @param {string} optionsHash The hash of the rewrite options. |
| * @param {boolean} sendBeaconAtOnload Controls whether the beacon should be |
| * sent at page onload. This should be set to false if lazyload is also |
| * enabled, since we want to wait to check all images on the page until they |
| * have finished running their onload handlers. |
| * @param {boolean} checkRenderedImageSizes The bool to show if resizing is |
| * being done using the rendered dimensions. If yes we capture the rendered |
| * dimensions and send it back with the beacon. |
| * @param {string} nonce The nonce sent by the server. |
| * @export |
| */ |
| pagespeed.CriticalImages.Run = function( |
| beaconUrl, htmlUrl, optionsHash, sendBeaconAtOnload, |
| checkRenderedImageSizes, nonce) { |
| var beacon = new pagespeed.CriticalImages.Beacon_( |
| beaconUrl, htmlUrl, optionsHash, checkRenderedImageSizes, nonce); |
| pagespeed.CriticalImages.beaconObj_ = beacon; |
| // If lazyload is enabled on the page, then it will handle calling the beacon |
| // when all images have finished loading. Otherwise, call at onload when all |
| // images are loaded. |
| if (sendBeaconAtOnload) { |
| var beaconOnload = function() { |
| // Attempt not to block other onload events on the page by wrapping in |
| // setTimeout(). |
| window.setTimeout(function() { beacon.checkCriticalImages_(); }, 0); |
| }; |
| pagespeedutils.addHandler(window, 'load', beaconOnload); |
| } |
| }; |