blob: 3a2a7326405929c87dd3c7b8bf69e181469eef6f [file] [log] [blame]
/*
* Copyright 2014 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 JS class for finding below-the-fold XPaths used by split_html.
*/
goog.provide('pagespeedutils.CriticalXPaths');
goog.provide('pagespeedutils.generateXPath');
goog.require('pagespeedutils');
/**
* @constructor
* @param {number} viewportWidth Width of the viewport.
* @param {number} viewportHeight Height of the viewport.
* @param {Node} document The root document.
*/
pagespeedutils.CriticalXPaths = function(
viewportWidth, viewportHeight, document) {
/**
* Viewport size.
* @type {{ height: (number), width: (number) }}
* @private
*/
this.windowSize_ = {
'height': viewportHeight,
'width': viewportWidth
};
/**
* List of xpath pairs in the form start_xpath:end_xpath.
* @type {Array.<string>}
* @private
*/
this.xpathPairs_ = [];
this.document_ = document;
};
/**
* Gets the start and end xpath for the non-critical panels.
* @return {Array.<string>}
*/
pagespeedutils.CriticalXPaths.prototype.getNonCriticalPanelXPathPairs =
function() {
// We only need to mark elements in the body. Elements in the head are never
// visible.
this.findNonCriticalPanelBoundaries_(this.document_.body);
return this.xpathPairs_;
};
/**
* Appends Appends a new XPath to the current list of XPath pairs.
* @param {string} startXpath Start XPath.
* @param {string} endXpath End XPath.
* @private
*/
pagespeedutils.CriticalXPaths.prototype.addXPathPair_ = function(startXpath,
endXpath) {
if (!startXpath) {
return;
}
var xpathPair = startXpath;
if (endXpath) {
xpathPair += ':' + endXpath;
}
this.xpathPairs_.push(xpathPair);
};
/**
* Identifies all nodes which are not in viewport and populates the start and
* end xpath of non-critical panels into xpathPairs_.
* @param {!Node} node Dom node.
* @return {boolean} true iff node in viewport or any of node's
* descendants is critical.
* @private
*/
pagespeedutils.CriticalXPaths.prototype.findNonCriticalPanelBoundaries_ =
function(node) {
var nodeIsCritical = pagespeedutils.inViewport(node, this.windowSize_);
var prevChildIsCritical = nodeIsCritical;
// The loop below determines the XPaths at the transition points of nodes
// going from critical to non-critical. startChildXPath will be the first
// non-critical node, and endChildXPath will be the first critical node after
// the non-critical section.
var startChildXPath = '';
var endChildXPath = '';
// Iterate over the visible (that is, not hidden, independent of viewport
// size) sibling nodes of the passed in node.
var firstChild = this.visibleNodeOrSibling_(node.firstChild);
for (var currNode = firstChild; currNode != null;
currNode = this.visibleNodeOrSibling_(currNode.nextSibling)) {
// Check if we are at a criticality transition point by making a recursive
// call to findNonCriticalPanelBoundaries for the current sibling node. If
// we are at a transition point from critical to non-critical, we need to
// mark the current sibling node as the start of an XPath. If we are
// transitioning from non-critical to critical, then the current node is the
// end of our XPath, and we need to store the complete path.
// TODO(jud): The logic here is tricky to follow. See if we can simplify
// this to make it clearer and more efficient.
var currNodeIsCritical = this.findNonCriticalPanelBoundaries_(currNode);
if (currNodeIsCritical != prevChildIsCritical) {
if (!currNodeIsCritical) {
// Going from critical -> non-critical.
startChildXPath = pagespeedutils.generateXPath(
currNode, this.document_);
endChildXPath = '';
} else {
// Going from non-critical -> critical
if (!nodeIsCritical) {
// But non-critical panel never began.
// All the nodes till currNode are non-critical.
if (firstChild != currNode) {
startChildXPath = pagespeedutils.generateXPath(
firstChild, this.document_);
}
nodeIsCritical = true;
}
endChildXPath = pagespeedutils.generateXPath(currNode, this.document_);
if (!endChildXPath) {
// If 'endChildXPath' is empty here, then it is better not to split
// here than treating few critical nodes as non-critical.
startChildXPath = '';
}
if (startChildXPath) {
this.addXPathPair_(startChildXPath, endChildXPath);
}
startChildXPath = '';
}
prevChildIsCritical = currNodeIsCritical;
}
}
if (startChildXPath) {
this.addXPathPair_(startChildXPath, endChildXPath);
}
return nodeIsCritical;
};
/**
* Generates the xpath for the given node which is relative to body.
* @param {Node} node Node in dom tree whose xpath is generated.
* @param {Node} doc Page document.
* @return {string} xpath of a node which is always a rendered one.
*/
pagespeedutils.generateXPath = function(node, doc) {
var xpathUnits = [];
while (node != doc.body) {
var id = node.getAttribute('id');
if (id && doc.querySelectorAll('#' + id).length == 1) {
// Use 'id' inside xpath only if it is used once in the document.
xpathUnits.unshift(node.tagName.toLowerCase() + '[@id=\"' +
node.getAttribute('id') + '\"]');
break;
} else {
var i = 0;
for (var sibling = node;
sibling;
sibling = sibling.previousElementSibling) {
// Please keep this list of tags same as that in
// google_critical_line_info_finder.cc.
if ((sibling.tagName === 'SCRIPT') ||
(sibling.tagName === 'NOSCRIPT') ||
(sibling.tagName === 'STYLE') ||
(sibling.tagName === 'LINK')) {
continue;
}
++i;
}
xpathUnits.unshift(node.tagName.toLowerCase() + '[' + i + ']');
}
node = node.parentNode;
}
return xpathUnits.length ? xpathUnits.join('/') : '';
};
/**
* @param {Node} node Node in DOM tree.
* @return {?Node} Returns node if it is visible (as in width and height greater
* than 0, irrespective of the viewport), or the first visible sibling of
* node if it isn't. Returns null if no valid nodes are found.
* @private
*/
pagespeedutils.CriticalXPaths.prototype.visibleNodeOrSibling_ = function(node) {
for (; node != null; node = node.nextSibling) {
if (node.nodeType != Node.ELEMENT_NODE) continue;
if (node.offsetWidth == 0 && node.offsetHeight == 0) continue;
// Also check node.offsetParent to see if the node is hidden. offsetParent
// is null when the node has display:none or position:fixed styling, or if
// it's a body node. To differentiate, we explicitly check for
// position:fixed and body and do not consider those cases hidden.
if (node.offsetParent || node.tagName == 'BODY' ||
node.style.position == 'fixed') return node;
}
return null;
};