/* * Copyright (C) 2010 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ WebInspector.AuditRules.IPAddressRegexp = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; WebInspector.AuditRules.CacheableResponseCodes = { 200: true, 203: true, 206: true, 300: true, 301: true, 410: true, 304: true // Underlying resource is cacheable } /** * @param {Array} array Array of Elements (outerHTML is used) or strings (plain value is used as innerHTML) */ WebInspector.AuditRules.arrayAsUL = function(array, shouldLinkify) { if (!array.length) return ""; var ulElement = document.createElement("ul"); for (var i = 0; i < array.length; ++i) { var liElement = document.createElement("li"); if (array[i] instanceof Element) liElement.appendChild(array[i]); else if (shouldLinkify) liElement.appendChild(WebInspector.linkifyURLAsNode(array[i])); else liElement.innerHTML = array[i]; ulElement.appendChild(liElement); } return ulElement.outerHTML; } WebInspector.AuditRules.getDomainToResourcesMap = function(resources, types, regexp, needFullResources) { var domainToResourcesMap = {}; for (var i = 0, size = resources.length; i < size; ++i) { var resource = resources[i]; if (types && types.indexOf(resource.type) === -1) continue; var match = resource.url.match(regexp); if (!match) continue; var domain = match[2]; var domainResources = domainToResourcesMap[domain]; if (domainResources === undefined) { domainResources = []; domainToResourcesMap[domain] = domainResources; } domainResources.push(needFullResources ? resource : resource.url); } return domainToResourcesMap; } WebInspector.AuditRules.evaluateInTargetWindow = function(func, callback) { InjectedScriptAccess.getDefault().evaluateOnSelf(func.toString(), callback); } WebInspector.AuditRules.GzipRule = function() { WebInspector.AuditRule.call(this, "network-gzip", "Enable gzip compression"); } WebInspector.AuditRules.GzipRule.prototype = { doRun: function(resources, result, callback) { try { var commonMessage = undefined; var totalSavings = 0; var compressedSize = 0 var candidateSize = 0 var outputResources = []; for (var i = 0, length = resources.length; i < length; ++i) { var resource = resources[i]; if (this._shouldCompress(resource)) { var size = resource.contentLength; candidateSize += size; if (this._isCompressed(resource)) { compressedSize += size; continue; } if (!commonMessage) commonMessage = result.appendChild(""); var savings = 2 * size / 3; totalSavings += savings; outputResources.push( String.sprintf("Compressing %s could save ~%s", WebInspector.linkifyURL(resource.url), Number.bytesToString(savings))); } } if (commonMessage) { commonMessage.value = String.sprintf("Compressing the following resources with gzip could reduce their " + "transfer size by about two thirds (~%s):", Number.bytesToString(totalSavings)); commonMessage.appendChild(WebInspector.AuditRules.arrayAsUL(outputResources)); result.score = 100 * compressedSize / candidateSize; result.type = WebInspector.AuditRuleResult.Type.Violation; } } catch(e) { console.log(e); } finally { callback(result); } }, _isCompressed: function(resource) { var encoding = resource.responseHeaders["Content-Encoding"]; return encoding === "gzip" || encoding === "deflate"; }, _shouldCompress: function(resource) { return WebInspector.Resource.Type.isTextType(resource.type) && resource.domain && resource.contentLength !== undefined && resource.contentLength > 150; } } WebInspector.AuditRules.GzipRule.prototype.__proto__ = WebInspector.AuditRule.prototype; WebInspector.AuditRules.CombineExternalResourcesRule = function(id, name, type, resourceTypeName, parametersObject) { WebInspector.AuditRule.call(this, id, name, parametersObject); this._type = type; this._resourceTypeName = resourceTypeName; } WebInspector.AuditRules.CombineExternalResourcesRule.prototype = { doRun: function(resources, result, callback) { try { var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, [this._type], WebInspector.URLRegExp); var penalizedResourceCount = 0; // TODO: refactor according to the chosen i18n approach for (var domain in domainToResourcesMap) { var domainResources = domainToResourcesMap[domain]; var extraResourceCount = domainResources.length - this.getValue("AllowedPerDomain"); if (extraResourceCount <= 0) continue; penalizedResourceCount += extraResourceCount - 1; result.appendChild( String.sprintf("There are %d %s files served from %s. Consider combining them into as few files as possible.", domainResources.length, this._resourceTypeName, domain)); } result.score = 100 - (penalizedResourceCount * this.getValue("ScorePerResource")); result.type = WebInspector.AuditRuleResult.Type.Hint; } catch(e) { console.log(e); } finally { callback(result); } } }; WebInspector.AuditRules.CombineExternalResourcesRule.prototype.__proto__ = WebInspector.AuditRule.prototype; WebInspector.AuditRules.CombineJsResourcesRule = function(parametersObject) { WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externaljs", "Combine external JavaScript", WebInspector.Resource.Type.Script, "JS", parametersObject); } WebInspector.AuditRules.CombineJsResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype; WebInspector.AuditRules.CombineCssResourcesRule = function(parametersObject) { WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externalcss", "Combine external CSS", WebInspector.Resource.Type.Stylesheet, "CSS", parametersObject); } WebInspector.AuditRules.CombineCssResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype; WebInspector.AuditRules.MinimizeDnsLookupsRule = function(parametersObject) { WebInspector.AuditRule.call(this, "network-minimizelookups", "Minimize DNS lookups", parametersObject); } WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype = { doRun: function(resources, result, callback) { try { var violationDomains = []; var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, undefined, WebInspector.URLRegExp); for (var domain in domainToResourcesMap) { if (domainToResourcesMap[domain].length > 1) continue; var match = domain.match(WebInspector.URLRegExp); if (!match) continue; if (!match[2].search(WebInspector.AuditRules.IPAddressRegexp)) continue; // an IP address violationDomains.push(match[2]); } if (violationDomains.length <= this.getValue("HostCountThreshold")) return; var commonMessage = result.appendChild( "The following domains only serve one resource each. If possible, avoid the extra DNS " + "lookups by serving these resources from existing domains."); commonMessage.appendChild(WebInspector.AuditRules.arrayAsUL(violationDomains)); result.score = 100 - violationDomains.length * this.getValue("ViolationDomainScore"); } catch(e) { console.log(e); } finally { callback(result); } } } WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype.__proto__ = WebInspector.AuditRule.prototype; WebInspector.AuditRules.ParallelizeDownloadRule = function(parametersObject) { WebInspector.AuditRule.call(this, "network-parallelizehosts", "Parallelize downloads across hostnames", parametersObject); } WebInspector.AuditRules.ParallelizeDownloadRule.prototype = { doRun: function(resources, result, callback) { function hostSorter(a, b) { var aCount = domainToResourcesMap[a].length; var bCount = domainToResourcesMap[b].length; return (aCount < bCount) ? 1 : (aCount == bCount) ? 0 : -1; } try { var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap( resources, [WebInspector.Resource.Type.Stylesheet, WebInspector.Resource.Type.Image], WebInspector.URLRegExp, true); var hosts = []; for (var url in domainToResourcesMap) hosts.push(url); if (!hosts.length) return; // no hosts (local file or something) hosts.sort(hostSorter); var optimalHostnameCount = this.getValue("OptimalHostnameCount"); if (hosts.length > optimalHostnameCount) hosts.splice(optimalHostnameCount); var busiestHostResourceCount = domainToResourcesMap[hosts[0]].length; var resourceCountAboveThreshold = busiestHostResourceCount - this.getValue("MinRequestThreshold"); if (resourceCountAboveThreshold <= 0) return; var avgResourcesPerHost = 0; for (var i = 0, size = hosts.length; i < size; ++i) avgResourcesPerHost += domainToResourcesMap[hosts[i]].length; // Assume optimal parallelization. avgResourcesPerHost /= optimalHostnameCount; avgResourcesPerHost = Math.max(avgResourcesPerHost, 1); var pctAboveAvg = (resourceCountAboveThreshold / avgResourcesPerHost) - 1.0; var minBalanceThreshold = this.getValue("MinBalanceThreshold"); if (pctAboveAvg < minBalanceThreshold) { result.score = 100; return; } result.score = (1 - (pctAboveAvg - minBalanceThreshold)) * 100; result.type = WebInspector.AuditRuleResult.Type.Hint; var resourcesOnBusiestHost = domainToResourcesMap[hosts[0]]; var commonMessage = result.appendChild( String.sprintf("This page makes %d parallelizable requests to %s" + ". Increase download parallelization by distributing the following" + " requests across multiple hostnames.", busiestHostResourceCount, hosts[0])); var outputResources = []; for (var i = 0, size = resourcesOnBusiestHost.length; i < size; ++i) outputResources.push(resourcesOnBusiestHost[i].url); commonMessage.appendChild(WebInspector.AuditRules.arrayAsUL(outputResources, true)); } catch(e) { console.log(e); } finally { callback(result); } } } WebInspector.AuditRules.ParallelizeDownloadRule.prototype.__proto__ = WebInspector.AuditRule.prototype; // The reported CSS rule size is incorrect (parsed != original in WebKit), // so use percentages instead, which gives a better approximation. WebInspector.AuditRules.UnusedCssRule = function(parametersObject) { WebInspector.AuditRule.call(this, "page-unusedcss", "Remove unused CSS", parametersObject); } WebInspector.AuditRules.UnusedCssRule.prototype = { _getUnusedStylesheetRatioMessage: function(unusedLength, type, location, styleSheetLength) { var url = type === "href" ? WebInspector.linkifyURL(location) : String.sprintf("Inline block #%s", location); var pctUnused = Math.round(unusedLength / styleSheetLength * 100); return String.sprintf("%s: %f%% (estimated) is not used by the current page.", url, pctUnused); }, _getUnusedTotalRatioMessage: function(unusedLength, totalLength) { var pctUnused = Math.round(unusedLength / totalLength * 100); return String.sprintf("%d%% of CSS (estimated) is not used by the current page.", pctUnused); }, doRun: function(resources, result, callback) { var self = this; function evalCallback(evalResult, isException) { try { if (isException) return; var totalLength = 0; var totalUnusedLength = 0; var topMessage; var styleSheetMessage; for (var i = 0; i < evalResult.length; ) { var type = evalResult[i++]; if (type === "totalLength") { totalLength = evalResult[i++]; continue; } var styleSheetLength = evalResult[i++]; var location = evalResult[i++]; var unusedRules = evalResult[i++]; styleSheetMessage = undefined; if (!topMessage) topMessage = result.appendChild(""); var totalUnusedRuleLength = 0; var ruleSelectors = []; for (var j = 0; j < unusedRules.length; ++j) { var rule = unusedRules[j]; totalUnusedRuleLength += parseInt(rule[1]); if (!styleSheetMessage) styleSheetMessage = result.appendChild(""); ruleSelectors.push(rule[0]); } styleSheetMessage.appendChild(WebInspector.AuditRules.arrayAsUL(ruleSelectors)); styleSheetMessage.value = self._getUnusedStylesheetRatioMessage(totalUnusedRuleLength, type, location, styleSheetLength); totalUnusedLength += totalUnusedRuleLength; } if (totalUnusedLength) { var totalUnusedPercent = totalUnusedLength / totalLength; topMessage.value = self._getUnusedTotalRatioMessage(totalUnusedLength, totalLength); var pctMultiplier = Math.log(Math.max(200, totalUnusedLength - 800)) / 7 - 0.6; result.score = (1 - totalUnusedPercent * pctMultiplier) * 100; result.type = WebInspector.AuditRuleResult.Type.Hint; } else result.score = 100; } catch(e) { console.log(e); } finally { callback(result); } } function routine() { var styleSheets = document.styleSheets; if (!styleSheets) return {}; var styleSheetToUnusedRules = []; var inlineBlockOrdinal = 0; var totalCSSLength = 0; var pseudoSelectorRegexp = /:hover|:link|:active|:visited|:focus/; for (var i = 0; i < styleSheets.length; ++i) { var styleSheet = styleSheets[i]; if (!styleSheet.cssRules) continue; var currentStyleSheetSize = 0; var unusedRules = []; for (var curRule = 0; curRule < styleSheet.cssRules.length; ++curRule) { var rule = styleSheet.cssRules[curRule]; var textLength = rule.cssText ? rule.cssText.length : 0; currentStyleSheetSize += textLength; totalCSSLength += textLength; if (rule.type !== 1 || rule.selectorText.match(pseudoSelectorRegexp)) continue; var nodes = document.querySelectorAll(rule.selectorText); if (nodes && nodes.length) continue; unusedRules.push([rule.selectorText, textLength]); } if (unusedRules.length) { styleSheetToUnusedRules.push(styleSheet.href ? "href" : "inline"); styleSheetToUnusedRules.push(currentStyleSheetSize); styleSheetToUnusedRules.push(styleSheet.href ? styleSheet.href : ++inlineBlockOrdinal); styleSheetToUnusedRules.push(unusedRules); } } styleSheetToUnusedRules.push("totalLength"); styleSheetToUnusedRules.push(totalCSSLength); return styleSheetToUnusedRules; } WebInspector.AuditRules.evaluateInTargetWindow(routine, evalCallback); } } WebInspector.AuditRules.UnusedCssRule.prototype.__proto__ = WebInspector.AuditRule.prototype; WebInspector.AuditRules.CacheControlRule = function(id, name, parametersObject) { WebInspector.AuditRule.call(this, id, name, parametersObject); } WebInspector.AuditRules.CacheControlRule.MillisPerMonth = 1000 * 60 * 60 * 24 * 30; WebInspector.AuditRules.CacheControlRule.prototype = { InfoCheck: -1, FailCheck: 0, WarningCheck: 1, SevereCheck: 2, doRun: function(resources, result, callback) { try { var cacheableAndNonCacheableResources = this._cacheableAndNonCacheableResources(resources); if (cacheableAndNonCacheableResources[0].length) { result.score = 100; this.runChecks(cacheableAndNonCacheableResources[0], result); } this.handleNonCacheableResources(cacheableAndNonCacheableResources[1], result); } catch(e) { console.log(e); } finally { callback(result); } }, handleNonCacheableResources: function() { }, _cacheableAndNonCacheableResources: function(resources) { var processedResources = [[], []]; for (var i = 0; i < resources.length; ++i) { var resource = resources[i]; if (!this.isCacheableResource(resource)) continue; if (this._isExplicitlyNonCacheable(resource)) processedResources[1].push(resource); else processedResources[0].push(resource); } return processedResources; }, execCheck: function(messageText, resourceCheckFunction, resources, severity, result) { var topMessage; var failingResources = 0; var resourceCount = resources.length; var outputResources = []; for (var i = 0; i < resourceCount; ++i) { if (resourceCheckFunction.call(this, resources[i])) { ++failingResources; if (!topMessage) topMessage = result.appendChild(messageText); outputResources.push(resources[i].url); } } if (topMessage) topMessage.appendChild(WebInspector.AuditRules.arrayAsUL(outputResources, true)); if (failingResources) { switch (severity) { case this.FailCheck: result.score = 0; result.type = WebInspector.AuditRuleResult.Type.Violation; break; case this.SevereCheck: case this.WarningCheck: result.score -= 50 * severity * failingResources / resourceCount; result.type = WebInspector.AuditRuleResult.Type.Hint; break; } } return topMessage; }, freshnessLifetimeGreaterThan: function(resource, timeMs) { var dateHeader = this.responseHeader(resource, "Date"); if (!dateHeader) return false; var dateHeaderMs = Date.parse(dateHeader); if (isNaN(dateHeaderMs)) return false; var freshnessLifetimeMs; var maxAgeMatch = this.responseHeaderMatch(resource, "Cache-Control", "max-age=(\\d+)"); if (maxAgeMatch) freshnessLifetimeMs = (maxAgeMatch[1]) ? 1000 * maxAgeMatch[1] : 0; else { var expiresHeader = this.responseHeader(resource, "Expires"); if (expiresHeader) { var expDate = Date.parse(expiresHeader); if (!isNaN(expDate)) freshnessLifetimeMs = expDate - dateHeaderMs; } } return (isNaN(freshnessLifetimeMs)) ? false : freshnessLifetimeMs > timeMs; }, responseHeader: function(resource, header) { return resource.responseHeaders[header]; }, hasResponseHeader: function(resource, header) { return resource.responseHeaders[header] !== undefined; }, isCompressible: function(resource) { return WebInspector.Resource.Type.isTextType(resource.type); }, isPubliclyCacheable: function(resource) { if (this._isExplicitlyNonCacheable(resource)) return false; if (this.responseHeaderMatch(resource, "Cache-Control", "public")) return true; return resource.url.indexOf("?") == -1 && !this.responseHeaderMatch(resource, "Cache-Control", "private"); }, responseHeaderMatch: function(resource, header, regexp) { return resource.responseHeaders[header] ? resource.responseHeaders[header].match(new RegExp(regexp, "im")) : undefined; }, hasExplicitExpiration: function(resource) { return this.hasResponseHeader(resource, "Date") && (this.hasResponseHeader(resource, "Expires") || this.responseHeaderMatch(resource, "Cache-Control", "max-age")); }, _isExplicitlyNonCacheable: function(resource) { var hasExplicitExp = this.hasExplicitExpiration(resource); return this.responseHeaderMatch(resource, "Cache-Control", "(no-cache|no-store|must-revalidate)") || this.responseHeaderMatch(resource, "Pragma", "no-cache") || (hasExplicitExp && !this.freshnessLifetimeGreaterThan(resource, 0)) || (!hasExplicitExp && resource.url && resource.url.indexOf("?") >= 0) || (!hasExplicitExp && !this.isCacheableResource(resource)); }, isCacheableResource: function(resource) { return resource.statusCode !== undefined && WebInspector.AuditRules.CacheableResponseCodes[resource.statusCode]; } } WebInspector.AuditRules.CacheControlRule.prototype.__proto__ = WebInspector.AuditRule.prototype; WebInspector.AuditRules.BrowserCacheControlRule = function(parametersObject) { WebInspector.AuditRules.CacheControlRule.call(this, "http-browsercache", "Leverage browser caching", parametersObject); } WebInspector.AuditRules.BrowserCacheControlRule.prototype = { handleNonCacheableResources: function(resources, result) { if (resources.length) { var message = result.appendChild( "The following resources are explicitly non-cacheable. Consider making them cacheable if possible:"); var resourceOutput = []; for (var i = 0; i < resources.length; ++i) resourceOutput.push(resources[i].url); message.appendChild(WebInspector.AuditRules.arrayAsUL(resourceOutput, true)); } }, runChecks: function(resources, result, callback) { this.execCheck( "The following resources are missing a cache expiration." + " Resources that do not specify an expiration may not be" + " cached by browsers:", this._missingExpirationCheck, resources, this.SevereCheck, result); this.execCheck( "The following resources specify a \"Vary\" header that" + " disables caching in most versions of Internet Explorer:", this._varyCheck, resources, this.SevereCheck, result); this.execCheck( "The following cacheable resources have a short" + " freshness lifetime:", this._oneMonthExpirationCheck, resources, this.WarningCheck, result); // Unable to implement the favicon check due to the WebKit limitations. this.execCheck( "To further improve cache hit rate, specify an expiration" + " one year in the future for the following cacheable" + " resources:", this._oneYearExpirationCheck, resources, this.InfoCheck, result); }, _missingExpirationCheck: function(resource) { return this.isCacheableResource(resource) && !this.hasResponseHeader(resource, "Set-Cookie") && !this.hasExplicitExpiration(resource); }, _varyCheck: function(resource) { var varyHeader = this.responseHeader(resource, "Vary"); if (varyHeader) { varyHeader = varyHeader.replace(/User-Agent/gi, ""); varyHeader = varyHeader.replace(/Accept-Encoding/gi, ""); varyHeader = varyHeader.replace(/[, ]*/g, ""); } return varyHeader && varyHeader.length && this.isCacheableResource(resource) && this.freshnessLifetimeGreaterThan(resource, 0); }, _oneMonthExpirationCheck: function(resource) { return this.isCacheableResource(resource) && !this.hasResponseHeader(resource, "Set-Cookie") && !this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth) && this.freshnessLifetimeGreaterThan(resource, 0); }, _oneYearExpirationCheck: function(resource) { return this.isCacheableResource(resource) && !this.hasResponseHeader(resource, "Set-Cookie") && !this.freshnessLifetimeGreaterThan(resource, 11 * WebInspector.AuditRules.CacheControlRule.MillisPerMonth) && this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth); } } WebInspector.AuditRules.BrowserCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype; WebInspector.AuditRules.ProxyCacheControlRule = function(parametersObject) { WebInspector.AuditRules.CacheControlRule.call(this, "http-proxycache", "Leverage proxy caching", parametersObject); } WebInspector.AuditRules.ProxyCacheControlRule.prototype = { runChecks: function(resources, result, callback) { this.execCheck( "Resources with a \"?\" in the URL are not cached by most" + " proxy caching servers:", this._questionMarkCheck, resources, this.WarningCheck, result); this.execCheck( "Consider adding a \"Cache-Control: public\" header to the" + " following resources:", this._publicCachingCheck, resources, this.InfoCheck, result); this.execCheck( "The following publicly cacheable resources contain" + " a Set-Cookie header. This security vulnerability" + " can cause cookies to be shared by multiple users.", this._setCookieCacheableCheck, resources, this.FailCheck, result); }, _questionMarkCheck: function(resource) { return resource.url.indexOf("?") >= 0 && !this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource); }, _publicCachingCheck: function(resource) { return this.isCacheableResource(resource) && !this.isCompressible(resource) && !this.responseHeaderMatch(resource, "Cache-Control", "public") && !this.hasResponseHeader(resource, "Set-Cookie"); }, _setCookieCacheableCheck: function(resource) { return this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource); } } WebInspector.AuditRules.ProxyCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype; WebInspector.AuditRules.ImageDimensionsRule = function(parametersObject) { WebInspector.AuditRule.call(this, "page-imagedims", "Specify image dimensions", parametersObject); } WebInspector.AuditRules.ImageDimensionsRule.prototype = { doRun: function(resources, result, callback) { function evalCallback(evalResult, isException) { try { if (isException) return; if (!evalResult || !evalResult.totalImages) return; result.score = 100; var topMessage = result.appendChild( "A width and height should be specified for all images in order to " + "speed up page display. The following image(s) are missing a width and/or height:"); var map = evalResult.map; var outputResources = []; for (var url in map) { var value = WebInspector.linkifyURL(url); if (map[url] > 1) value += " (" + map[url] + " uses)"; outputResources.push(value); result.score -= this.getValue("ScorePerImageUse") * map[url]; result.type = WebInspector.AuditRuleResult.Type.Hint; } topMessage.appendChild(WebInspector.AuditRules.arrayAsUL(outputResources)); } catch(e) { console.log(e); } finally { callback(result); } } function routine() { var images = document.getElementsByTagName("img"); const widthRegExp = /width[^:;]*:/gim; const heightRegExp = /height[^:;]*:/gim; function hasDimension(element, cssText, rules, regexp, attributeName) { if (element.attributes.getNamedItem(attributeName) != null || (cssText && cssText.match(regexp))) return true; if (!rules) return false; for (var i = 0; i < rules.length; ++i) { if (rules.item(i).style.cssText.match(regexp)) return true; } return false; } function hasWidth(element, cssText, rules) { return hasDimension(element, cssText, rules, widthRegExp, "width"); } function hasHeight(element, cssText, rules) { return hasDimension(element, cssText, rules, heightRegExp, "height"); } var urlToNoDimensionCount = {}; var found = false; for (var i = 0; i < images.length; ++i) { var image = images[i]; if (!image.src) continue; var position = document.defaultView.getComputedStyle(image).getPropertyValue("position"); if (position === "absolute") continue; var cssText = (image.style && image.style.cssText) ? image.style.cssText : ""; var rules = document.defaultView.getMatchedCSSRules(image, "", true); if (!hasWidth(image, cssText, rules) || !hasHeight(image, cssText, rules)) { found = true; if (urlToNoDimensionCount.hasOwnProperty(image.src)) ++urlToNoDimensionCount[image.src]; else urlToNoDimensionCount[image.src] = 1; } } return found ? {totalImages: images.length, map: urlToNoDimensionCount} : null; } WebInspector.AuditRules.evaluateInTargetWindow(routine, evalCallback.bind(this)); } } WebInspector.AuditRules.ImageDimensionsRule.prototype.__proto__ = WebInspector.AuditRule.prototype; WebInspector.AuditRules.CssInHeadRule = function(parametersObject) { WebInspector.AuditRule.call(this, "page-cssinhead", "Put CSS in the document head", parametersObject); } WebInspector.AuditRules.CssInHeadRule.prototype = { doRun: function(resources, result, callback) { function evalCallback(evalResult, isException) { try { if (isException) return; if (!evalResult) return; result.score = 100; var outputMessages = []; for (var url in evalResult) { var urlViolations = evalResult[url]; var topMessage = result.appendChild( String.sprintf("CSS in the %s document body adversely impacts rendering performance.", WebInspector.linkifyURL(url))); if (urlViolations[0]) { outputMessages.push( String.sprintf("%s style block(s) in the body should be moved to the document head.", urlViolations[0])); result.score -= this.getValue("InlineURLScore") * urlViolations[0]; } for (var i = 0; i < urlViolations[1].length; ++i) { outputMessages.push( String.sprintf("Link node %s should be moved to the document head", WebInspector.linkifyURL(urlViolations[1]))); } result.score -= this.getValue("InlineStylesheetScore") * urlViolations[1]; result.type = WebInspector.AuditRuleResult.Type.Hint; } topMessage.appendChild(WebInspector.AuditRules.arrayAsUL(outputMessages)); } catch(e) { console.log(e); } finally { callback(result); } } function routine() { function allViews() { var views = [document.defaultView]; var curView = 0; while (curView < views.length) { var view = views[curView]; var frames = view.frames; for (var i = 0; i < frames.length; ++i) { if (frames[i] !== view) views.push(frames[i]); } ++curView; } return views; } var views = allViews(); var urlToViolationsArray = {}; var found = false; for (var i = 0; i < views.length; ++i) { var view = views[i]; if (!view.document) continue; var inlineStyles = view.document.querySelectorAll("body style"); var inlineStylesheets = view.document.querySelectorAll( "body link[rel~='stylesheet'][href]"); if (!inlineStyles.length && !inlineStylesheets.length) continue; found = true; var inlineStylesheetHrefs = []; for (var j = 0; j < inlineStylesheets.length; ++j) inlineStylesheetHrefs.push(inlineStylesheets[j].href); urlToViolationsArray[view.location.href] = [inlineStyles.length, inlineStylesheetHrefs]; } return found ? urlToViolationsArray : null; } WebInspector.AuditRules.evaluateInTargetWindow(routine, evalCallback); } } WebInspector.AuditRules.CssInHeadRule.prototype.__proto__ = WebInspector.AuditRule.prototype; WebInspector.AuditRules.StylesScriptsOrderRule = function(parametersObject) { WebInspector.AuditRule.call(this, "page-stylescriptorder", "Optimize the order of styles and scripts", parametersObject); } WebInspector.AuditRules.StylesScriptsOrderRule.prototype = { doRun: function(resources, result, callback) { function evalCallback(evalResult, isException) { try { if (isException) return; if (!evalResult) return; result.score = 100; var lateCssUrls = evalResult['late']; if (lateCssUrls) { var lateMessage = result.appendChild( 'The following external CSS files were included after ' + 'an external JavaScript file in the document head. To ' + 'ensure CSS files are downloaded in parallel, always ' + 'include external CSS before external JavaScript.'); lateMessage.appendChild(WebInspector.AuditRules.arrayAsUL(lateCssUrls, true)); result.score -= this.getValue("InlineBetweenResourcesScore") * lateCssUrls.length; result.type = WebInspector.AuditRuleResult.Type.Violation; } if (evalResult['cssBeforeInlineCount']) { var count = evalResult['cssBeforeInlineCount']; result.appendChild(count + ' inline script block' + (count > 1 ? 's were' : ' was') + ' found in the head between an ' + 'external CSS file and another resource. To allow parallel ' + 'downloading, move the inline script before the external CSS ' + 'file, or after the next resource.'); result.score -= this.getValue("CSSAfterJSURLScore") * count; result.type = WebInspector.AuditRuleResult.Type.Violation; } } catch(e) { console.log(e); } finally { callback(result); } } function routine() { var lateStyles = document.querySelectorAll( "head script[src] ~ link[rel~='stylesheet'][href]"); var stylesBeforeInlineScript = document.querySelectorAll( "head link[rel~='stylesheet'][href] ~ script:not([src])"); var resultObject; if (!lateStyles.length && !stylesBeforeInlineScript.length) resultObject = null; else { resultObject = {}; if (lateStyles.length) { lateStyleUrls = []; for (var i = 0; i < lateStyles.length; ++i) lateStyleUrls.push(lateStyles[i].href); resultObject["late"] = lateStyleUrls; } resultObject["cssBeforeInlineCount"] = stylesBeforeInlineScript.length; } return resultObject; } WebInspector.AuditRules.evaluateInTargetWindow(routine, evalCallback.bind(this)); } } WebInspector.AuditRules.StylesScriptsOrderRule.prototype.__proto__ = WebInspector.AuditRule.prototype; WebInspector.AuditRules.CookieRuleBase = function(id, name, parametersObject) { WebInspector.AuditRule.call(this, id, name, parametersObject); } WebInspector.AuditRules.CookieRuleBase.prototype = { doRun: function(resources, result, callback) { var self = this; function resultCallback(receivedCookies, isAdvanced) { try { self.processCookies(isAdvanced ? receivedCookies : [], resources, result); } catch(e) { console.log(e); } finally { callback(result); } } WebInspector.Cookies.getCookiesAsync(resultCallback); }, mapResourceCookies: function(resourcesByDomain, allCookies, callback) { for (var i = 0; i < allCookies.length; ++i) { for (var resourceDomain in resourcesByDomain) { if (WebInspector.Cookies.cookieDomainMatchesResourceDomain(allCookies[i].domain, resourceDomain)) this._callbackForResourceCookiePairs(resourcesByDomain[resourceDomain], allCookies[i], callback); } } }, _callbackForResourceCookiePairs: function(resources, cookie, callback) { if (!resources) return; for (var i = 0; i < resources.length; ++i) { if (WebInspector.Cookies.cookieMatchesResourceURL(cookie, resources[i].url)) callback(resources[i], cookie); } } } WebInspector.AuditRules.CookieRuleBase.prototype.__proto__ = WebInspector.AuditRule.prototype; WebInspector.AuditRules.CookieSizeRule = function(parametersObject) { WebInspector.AuditRules.CookieRuleBase.call(this, "http-cookiesize", "Minimize cookie size", parametersObject); } WebInspector.AuditRules.CookieSizeRule.prototype = { _average: function(cookieArray) { var total = 0; for (var i = 0; i < cookieArray.length; ++i) total += cookieArray[i].size; return cookieArray.length ? Math.round(total / cookieArray.length) : 0; }, _max: function(cookieArray) { var result = 0; for (var i = 0; i < cookieArray.length; ++i) result = Math.max(cookieArray[i].size, result); return result; }, processCookies: function(allCookies, resources, result) { function maxSizeSorter(a, b) { return b.maxCookieSize - a.maxCookieSize; } function avgSizeSorter(a, b) { return b.avgCookieSize - a.avgCookieSize; } var cookiesPerResourceDomain = {}; function collectorCallback(resource, cookie) { var cookies = cookiesPerResourceDomain[resource.domain]; if (!cookies) { cookies = []; cookiesPerResourceDomain[resource.domain] = cookies; } cookies.push(cookie); } if (!allCookies.length) return; var sortedCookieSizes = []; var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, null, WebInspector.URLRegExp, true); var matchingResourceData = {}; this.mapResourceCookies(domainToResourcesMap, allCookies, collectorCallback.bind(this)); result.score = 100; for (var resourceDomain in cookiesPerResourceDomain) { var cookies = cookiesPerResourceDomain[resourceDomain]; sortedCookieSizes.push({ domain: resourceDomain, avgCookieSize: this._average(cookies), maxCookieSize: this._max(cookies) }); } var avgAllCookiesSize = this._average(allCookies); var hugeCookieDomains = []; sortedCookieSizes.sort(maxSizeSorter); var maxBytesThreshold = this.getValue("MaxBytesThreshold"); var minBytesThreshold = this.getValue("MinBytesThreshold"); for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) { var maxCookieSize = sortedCookieSizes[i].maxCookieSize; if (maxCookieSize > maxBytesThreshold) hugeCookieDomains.push(sortedCookieSizes[i].domain + ": " + Number.bytesToString(maxCookieSize)); } var bigAvgCookieDomains = []; sortedCookieSizes.sort(avgSizeSorter); for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) { var domain = sortedCookieSizes[i].domain; var avgCookieSize = sortedCookieSizes[i].avgCookieSize; if (avgCookieSize > minBytesThreshold && avgCookieSize < maxBytesThreshold) bigAvgCookieDomains.push(domain + ": " + Number.bytesToString(avgCookieSize)); } result.appendChild("The average cookie size for all requests on this page is " + Number.bytesToString(avgAllCookiesSize)); var message; if (hugeCookieDomains.length) { result.score = 75; result.type = WebInspector.AuditRuleResult.Type.Violation; message = result.appendChild( String.sprintf("The following domains have a cookie size in excess of %d " + " bytes. This is harmful because requests with cookies larger than 1KB" + " typically cannot fit into a single network packet.", maxBytesThreshold)); message.appendChild(WebInspector.AuditRules.arrayAsUL(hugeCookieDomains)); } if (bigAvgCookieDomains.length) { this.score -= Math.max(0, avgAllCookiesSize - minBytesThreshold) / (minBytesThreshold - minBytesThreshold) / this.getValue("TotalPoints"); if (!result.type) result.type = WebInspector.AuditRuleResult.Type.Hint; message = result.appendChild( String.sprintf("The following domains have an average cookie size in excess of %d" + " bytes. Reducing the size of cookies" + " for these domains can reduce the time it takes to send requests.", minBytesThreshold)); message.appendChild(WebInspector.AuditRules.arrayAsUL(bigAvgCookieDomains)); } if (!bigAvgCookieDomains.length && !hugeCookieDomains.length) result.score = WebInspector.AuditCategoryResult.ScoreNA; } } WebInspector.AuditRules.CookieSizeRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype; WebInspector.AuditRules.StaticCookielessRule = function(parametersObject) { WebInspector.AuditRules.CookieRuleBase.call(this, "http-staticcookieless", "Serve static content from a cookieless domain", parametersObject); } WebInspector.AuditRules.StaticCookielessRule.prototype = { processCookies: function(allCookies, resources, result) { var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, [WebInspector.Resource.Type.Stylesheet, WebInspector.Resource.Type.Image], WebInspector.URLRegExp, true); var totalStaticResources = 0; var minResources = this.getValue("MinResources"); for (var domain in domainToResourcesMap) totalStaticResources += domainToResourcesMap[domain].length; if (totalStaticResources < minResources) return; var matchingResourceData = {}; this.mapResourceCookies(domainToResourcesMap, allCookies, this._collectorCallback.bind(this, matchingResourceData)); var badUrls = []; var cookieBytes = 0; for (var url in matchingResourceData) { badUrls.push(url); cookieBytes += matchingResourceData[url] } if (badUrls.length < minResources) return; result.score = 100; var badPoints = cookieBytes / 75; var violationPct = Math.max(badUrls.length / totalStaticResources, 0.6); badPoints *= violationPct; result.score -= badPoints; result.score = Math.max(result.score, 0); result.type = WebInspector.AuditRuleResult.Type.Violation; result.appendChild(String.sprintf("%s of cookies were sent with the following static resources.", Number.bytesToString(cookieBytes))); var message = result.appendChild("Serve these static resources from a domain that does not set cookies:"); message.appendChild(WebInspector.AuditRules.arrayAsUL(badUrls, true)); }, _collectorCallback: function(matchingResourceData, resource, cookie) { matchingResourceData[resource.url] = (matchingResourceData[resource.url] || 0) + cookie.size; } } WebInspector.AuditRules.StaticCookielessRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype;