2 * Copyright (C) 2010 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 WebInspector.AuditRules.IPAddressRegexp = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
33 WebInspector.AuditRules.CacheableResponseCodes =
42 304: true // Underlying resource is cacheable
45 WebInspector.AuditRules.getDomainToResourcesMap = function(resources, types, needFullResources)
47 var domainToResourcesMap = {};
48 for (var i = 0, size = resources.length; i < size; ++i) {
49 var resource = resources[i];
50 if (types && types.indexOf(resource.type) === -1)
52 var parsedURL = resource.url.asParsedURL();
55 var domain = parsedURL.host;
56 var domainResources = domainToResourcesMap[domain];
57 if (domainResources === undefined) {
59 domainToResourcesMap[domain] = domainResources;
61 domainResources.push(needFullResources ? resource : resource.url);
63 return domainToResourcesMap;
66 WebInspector.AuditRules.GzipRule = function()
68 WebInspector.AuditRule.call(this, "network-gzip", "Enable gzip compression");
71 WebInspector.AuditRules.GzipRule.prototype = {
72 doRun: function(resources, result, callback)
75 var compressedSize = 0;
76 var candidateSize = 0;
77 var summary = result.addChild("", true);
78 for (var i = 0, length = resources.length; i < length; ++i) {
79 var resource = resources[i];
80 if (resource.statusCode === 304)
81 continue; // Do not test 304 Not Modified resources as their contents are always empty.
82 if (this._shouldCompress(resource)) {
83 var size = resource.resourceSize;
84 candidateSize += size;
85 if (this._isCompressed(resource)) {
86 compressedSize += size;
89 var savings = 2 * size / 3;
90 totalSavings += savings;
91 summary.addChild(String.sprintf("%s could save ~%s", WebInspector.AuditRuleResult.linkifyDisplayName(resource.url), Number.bytesToString(savings)));
92 result.violationCount++;
96 return callback(null);
97 summary.value = String.sprintf("Compressing the following resources with gzip could reduce their transfer size by about two thirds (~%s):", Number.bytesToString(totalSavings));
101 _isCompressed: function(resource)
103 var encodingHeader = resource.responseHeaders["Content-Encoding"];
107 return /\b(?:gzip|deflate)\b/.test(encodingHeader);
110 _shouldCompress: function(resource)
112 return WebInspector.Resource.Type.isTextType(resource.type) && resource.domain && resource.resourceSize !== undefined && resource.resourceSize > 150;
116 WebInspector.AuditRules.GzipRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
119 WebInspector.AuditRules.CombineExternalResourcesRule = function(id, name, type, resourceTypeName, allowedPerDomain)
121 WebInspector.AuditRule.call(this, id, name);
123 this._resourceTypeName = resourceTypeName;
124 this._allowedPerDomain = allowedPerDomain;
127 WebInspector.AuditRules.CombineExternalResourcesRule.prototype = {
128 doRun: function(resources, result, callback)
130 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, [this._type]);
131 var penalizedResourceCount = 0;
132 // TODO: refactor according to the chosen i18n approach
133 var summary = result.addChild("", true);
134 for (var domain in domainToResourcesMap) {
135 var domainResources = domainToResourcesMap[domain];
136 var extraResourceCount = domainResources.length - this._allowedPerDomain;
137 if (extraResourceCount <= 0)
139 penalizedResourceCount += extraResourceCount - 1;
140 summary.addChild(String.sprintf("%d %s resources served from %s.", domainResources.length, this._resourceTypeName, WebInspector.AuditRuleResult.resourceDomain(domain)));
141 result.violationCount += domainResources.length;
143 if (!penalizedResourceCount)
144 return callback(null);
146 summary.value = "There are multiple resources served from same domain. Consider combining them into as few files as possible.";
151 WebInspector.AuditRules.CombineExternalResourcesRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
154 WebInspector.AuditRules.CombineJsResourcesRule = function(allowedPerDomain) {
155 WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externaljs", "Combine external JavaScript", WebInspector.Resource.Type.Script, "JavaScript", allowedPerDomain);
158 WebInspector.AuditRules.CombineJsResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype;
161 WebInspector.AuditRules.CombineCssResourcesRule = function(allowedPerDomain) {
162 WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externalcss", "Combine external CSS", WebInspector.Resource.Type.Stylesheet, "CSS", allowedPerDomain);
165 WebInspector.AuditRules.CombineCssResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype;
168 WebInspector.AuditRules.MinimizeDnsLookupsRule = function(hostCountThreshold) {
169 WebInspector.AuditRule.call(this, "network-minimizelookups", "Minimize DNS lookups");
170 this._hostCountThreshold = hostCountThreshold;
173 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype = {
174 doRun: function(resources, result, callback)
176 var summary = result.addChild("");
177 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, undefined);
178 for (var domain in domainToResourcesMap) {
179 if (domainToResourcesMap[domain].length > 1)
181 var parsedURL = domain.asParsedURL();
184 if (!parsedURL.host.search(WebInspector.AuditRules.IPAddressRegexp))
185 continue; // an IP address
186 summary.addSnippet(match[2]);
187 result.violationCount++;
189 if (!summary.children || summary.children.length <= this._hostCountThreshold)
190 return callback(null);
192 summary.value = "The following domains only serve one resource each. If possible, avoid the extra DNS lookups by serving these resources from existing domains.";
197 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
200 WebInspector.AuditRules.ParallelizeDownloadRule = function(optimalHostnameCount, minRequestThreshold, minBalanceThreshold)
202 WebInspector.AuditRule.call(this, "network-parallelizehosts", "Parallelize downloads across hostnames");
203 this._optimalHostnameCount = optimalHostnameCount;
204 this._minRequestThreshold = minRequestThreshold;
205 this._minBalanceThreshold = minBalanceThreshold;
209 WebInspector.AuditRules.ParallelizeDownloadRule.prototype = {
210 doRun: function(resources, result, callback)
212 function hostSorter(a, b)
214 var aCount = domainToResourcesMap[a].length;
215 var bCount = domainToResourcesMap[b].length;
216 return (aCount < bCount) ? 1 : (aCount == bCount) ? 0 : -1;
219 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(
221 [WebInspector.Resource.Type.Stylesheet, WebInspector.Resource.Type.Image],
225 for (var url in domainToResourcesMap)
229 return callback(null); // no hosts (local file or something)
231 hosts.sort(hostSorter);
233 var optimalHostnameCount = this._optimalHostnameCount;
234 if (hosts.length > optimalHostnameCount)
235 hosts.splice(optimalHostnameCount);
237 var busiestHostResourceCount = domainToResourcesMap[hosts[0]].length;
238 var resourceCountAboveThreshold = busiestHostResourceCount - this._minRequestThreshold;
239 if (resourceCountAboveThreshold <= 0)
240 return callback(null);
242 var avgResourcesPerHost = 0;
243 for (var i = 0, size = hosts.length; i < size; ++i)
244 avgResourcesPerHost += domainToResourcesMap[hosts[i]].length;
246 // Assume optimal parallelization.
247 avgResourcesPerHost /= optimalHostnameCount;
248 avgResourcesPerHost = Math.max(avgResourcesPerHost, 1);
250 var pctAboveAvg = (resourceCountAboveThreshold / avgResourcesPerHost) - 1.0;
251 var minBalanceThreshold = this._minBalanceThreshold;
252 if (pctAboveAvg < minBalanceThreshold)
253 return callback(null);
255 var resourcesOnBusiestHost = domainToResourcesMap[hosts[0]];
256 var entry = result.addChild(String.sprintf("This page makes %d parallelizable requests to %s. Increase download parallelization by distributing the following requests across multiple hostnames.", busiestHostResourceCount, hosts[0]), true);
257 for (var i = 0; i < resourcesOnBusiestHost.length; ++i)
258 entry.addURL(resourcesOnBusiestHost[i].url);
260 result.violationCount = resourcesOnBusiestHost.length;
265 WebInspector.AuditRules.ParallelizeDownloadRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
268 // The reported CSS rule size is incorrect (parsed != original in WebKit),
269 // so use percentages instead, which gives a better approximation.
270 WebInspector.AuditRules.UnusedCssRule = function()
272 WebInspector.AuditRule.call(this, "page-unusedcss", "Remove unused CSS rules");
275 WebInspector.AuditRules.UnusedCssRule.prototype = {
276 doRun: function(resources, result, callback)
280 function evalCallback(styleSheets) {
281 if (!styleSheets.length)
282 return callback(null);
284 var pseudoSelectorRegexp = /:hover|:link|:active|:visited|:focus|:before|:after/;
286 var testedSelectors = {};
287 for (var i = 0; i < styleSheets.length; ++i) {
288 var styleSheet = styleSheets[i];
289 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
290 var selectorText = styleSheet.rules[curRule].selectorText;
291 if (selectorText.match(pseudoSelectorRegexp) || testedSelectors[selectorText])
293 selectors.push(selectorText);
294 testedSelectors[selectorText] = 1;
298 function selectorsCallback(callback, styleSheets, testedSelectors, foundSelectors)
300 var inlineBlockOrdinal = 0;
301 var totalStylesheetSize = 0;
302 var totalUnusedStylesheetSize = 0;
305 for (var i = 0; i < styleSheets.length; ++i) {
306 var styleSheet = styleSheets[i];
307 var stylesheetSize = 0;
308 var unusedStylesheetSize = 0;
309 var unusedRules = [];
310 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
311 var rule = styleSheet.rules[curRule];
312 // Exact computation whenever source ranges are available.
313 var textLength = (rule.selectorRange && rule.style.range && rule.style.range.end) ? rule.style.range.end - rule.selectorRange.start + 1 : 0;
314 if (!textLength && rule.style.cssText)
315 textLength = rule.style.cssText.length + rule.selectorText.length;
316 stylesheetSize += textLength;
317 if (!testedSelectors[rule.selectorText] || foundSelectors[rule.selectorText])
319 unusedStylesheetSize += textLength;
320 unusedRules.push(rule.selectorText);
322 totalStylesheetSize += stylesheetSize;
323 totalUnusedStylesheetSize += unusedStylesheetSize;
325 if (!unusedRules.length)
328 var resource = WebInspector.resourceForURL(styleSheet.sourceURL);
329 var isInlineBlock = resource && resource.type == WebInspector.Resource.Type.Document;
330 var url = !isInlineBlock ? WebInspector.AuditRuleResult.linkifyDisplayName(styleSheet.sourceURL) : String.sprintf("Inline block #%d", ++inlineBlockOrdinal);
331 var pctUnused = Math.round(100 * unusedStylesheetSize / stylesheetSize);
333 summary = result.addChild("", true);
334 var entry = summary.addChild(String.sprintf("%s: %s (%d%%) is not used by the current page.", url, Number.bytesToString(unusedStylesheetSize), pctUnused));
336 for (var j = 0; j < unusedRules.length; ++j)
337 entry.addSnippet(unusedRules[j]);
339 result.violationCount += unusedRules.length;
342 if (!totalUnusedStylesheetSize)
343 return callback(null);
345 var totalUnusedPercent = Math.round(100 * totalUnusedStylesheetSize / totalStylesheetSize);
346 summary.value = String.sprintf("%s (%d%%) of CSS is not used by the current page.", Number.bytesToString(totalUnusedStylesheetSize), totalUnusedPercent);
351 var foundSelectors = {};
352 function queryCallback(boundSelectorsCallback, selector, styleSheets, testedSelectors, nodeId)
355 foundSelectors[selector] = true;
356 if (boundSelectorsCallback)
357 boundSelectorsCallback(foundSelectors);
360 function documentLoaded(selectors, document) {
361 for (var i = 0; i < selectors.length; ++i)
362 WebInspector.domAgent.querySelector(document.id, selectors[i], queryCallback.bind(null, i === selectors.length - 1 ? selectorsCallback.bind(null, callback, styleSheets, testedSelectors) : null, selectors[i], styleSheets, testedSelectors));
365 WebInspector.domAgent.requestDocument(documentLoaded.bind(null, selectors));
368 function styleSheetCallback(styleSheets, sourceURL, continuation, styleSheet)
371 styleSheet.sourceURL = sourceURL;
372 styleSheets.push(styleSheet);
375 continuation(styleSheets);
378 function allStylesCallback(error, styleSheetInfos)
380 if (error || !styleSheetInfos || !styleSheetInfos.length)
381 return evalCallback([]);
382 var styleSheets = [];
383 for (var i = 0; i < styleSheetInfos.length; ++i) {
384 var info = styleSheetInfos[i];
385 WebInspector.CSSStyleSheet.createForId(info.styleSheetId, styleSheetCallback.bind(null, styleSheets, info.sourceURL, i == styleSheetInfos.length - 1 ? evalCallback : null));
389 CSSAgent.getAllStyleSheets(allStylesCallback);
393 WebInspector.AuditRules.UnusedCssRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
396 WebInspector.AuditRules.CacheControlRule = function(id, name)
398 WebInspector.AuditRule.call(this, id, name);
401 WebInspector.AuditRules.CacheControlRule.MillisPerMonth = 1000 * 60 * 60 * 24 * 30;
403 WebInspector.AuditRules.CacheControlRule.prototype = {
405 doRun: function(resources, result, callback)
407 var cacheableAndNonCacheableResources = this._cacheableAndNonCacheableResources(resources);
408 if (cacheableAndNonCacheableResources[0].length)
409 this.runChecks(cacheableAndNonCacheableResources[0], result);
410 this.handleNonCacheableResources(cacheableAndNonCacheableResources[1], result);
415 handleNonCacheableResources: function()
419 _cacheableAndNonCacheableResources: function(resources)
421 var processedResources = [[], []];
422 for (var i = 0; i < resources.length; ++i) {
423 var resource = resources[i];
424 if (!this.isCacheableResource(resource))
426 if (this._isExplicitlyNonCacheable(resource))
427 processedResources[1].push(resource);
429 processedResources[0].push(resource);
431 return processedResources;
434 execCheck: function(messageText, resourceCheckFunction, resources, result)
436 var resourceCount = resources.length;
438 for (var i = 0; i < resourceCount; ++i) {
439 if (resourceCheckFunction.call(this, resources[i]))
440 urls.push(resources[i].url);
443 var entry = result.addChild(messageText, true);
445 result.violationCount += urls.length;
449 freshnessLifetimeGreaterThan: function(resource, timeMs)
451 var dateHeader = this.responseHeader(resource, "Date");
455 var dateHeaderMs = Date.parse(dateHeader);
456 if (isNaN(dateHeaderMs))
459 var freshnessLifetimeMs;
460 var maxAgeMatch = this.responseHeaderMatch(resource, "Cache-Control", "max-age=(\\d+)");
463 freshnessLifetimeMs = (maxAgeMatch[1]) ? 1000 * maxAgeMatch[1] : 0;
465 var expiresHeader = this.responseHeader(resource, "Expires");
467 var expDate = Date.parse(expiresHeader);
469 freshnessLifetimeMs = expDate - dateHeaderMs;
473 return (isNaN(freshnessLifetimeMs)) ? false : freshnessLifetimeMs > timeMs;
476 responseHeader: function(resource, header)
478 return resource.responseHeaders[header];
481 hasResponseHeader: function(resource, header)
483 return resource.responseHeaders[header] !== undefined;
486 isCompressible: function(resource)
488 return WebInspector.Resource.Type.isTextType(resource.type);
491 isPubliclyCacheable: function(resource)
493 if (this._isExplicitlyNonCacheable(resource))
496 if (this.responseHeaderMatch(resource, "Cache-Control", "public"))
499 return resource.url.indexOf("?") == -1 && !this.responseHeaderMatch(resource, "Cache-Control", "private");
502 responseHeaderMatch: function(resource, header, regexp)
504 return resource.responseHeaders[header]
505 ? resource.responseHeaders[header].match(new RegExp(regexp, "im"))
509 hasExplicitExpiration: function(resource)
511 return this.hasResponseHeader(resource, "Date") &&
512 (this.hasResponseHeader(resource, "Expires") || this.responseHeaderMatch(resource, "Cache-Control", "max-age"));
515 _isExplicitlyNonCacheable: function(resource)
517 var hasExplicitExp = this.hasExplicitExpiration(resource);
518 return this.responseHeaderMatch(resource, "Cache-Control", "(no-cache|no-store|must-revalidate)") ||
519 this.responseHeaderMatch(resource, "Pragma", "no-cache") ||
520 (hasExplicitExp && !this.freshnessLifetimeGreaterThan(resource, 0)) ||
521 (!hasExplicitExp && resource.url && resource.url.indexOf("?") >= 0) ||
522 (!hasExplicitExp && !this.isCacheableResource(resource));
525 isCacheableResource: function(resource)
527 return resource.statusCode !== undefined && WebInspector.AuditRules.CacheableResponseCodes[resource.statusCode];
531 WebInspector.AuditRules.CacheControlRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
534 WebInspector.AuditRules.BrowserCacheControlRule = function()
536 WebInspector.AuditRules.CacheControlRule.call(this, "http-browsercache", "Leverage browser caching");
539 WebInspector.AuditRules.BrowserCacheControlRule.prototype = {
540 handleNonCacheableResources: function(resources, result)
542 if (resources.length) {
543 var entry = result.addChild("The following resources are explicitly non-cacheable. Consider making them cacheable if possible:", true);
544 result.violationCount += resources.length;
545 for (var i = 0; i < resources.length; ++i)
546 entry.addURL(resources[i].url);
550 runChecks: function(resources, result, callback)
552 this.execCheck("The following resources are missing a cache expiration. Resources that do not specify an expiration may not be cached by browsers:",
553 this._missingExpirationCheck, resources, result);
554 this.execCheck("The following resources specify a \"Vary\" header that disables caching in most versions of Internet Explorer:",
555 this._varyCheck, resources, result);
556 this.execCheck("The following cacheable resources have a short freshness lifetime:",
557 this._oneMonthExpirationCheck, resources, result);
559 // Unable to implement the favicon check due to the WebKit limitations.
560 this.execCheck("To further improve cache hit rate, specify an expiration one year in the future for the following cacheable resources:",
561 this._oneYearExpirationCheck, resources, result);
564 _missingExpirationCheck: function(resource)
566 return this.isCacheableResource(resource) && !this.hasResponseHeader(resource, "Set-Cookie") && !this.hasExplicitExpiration(resource);
569 _varyCheck: function(resource)
571 var varyHeader = this.responseHeader(resource, "Vary");
573 varyHeader = varyHeader.replace(/User-Agent/gi, "");
574 varyHeader = varyHeader.replace(/Accept-Encoding/gi, "");
575 varyHeader = varyHeader.replace(/[, ]*/g, "");
577 return varyHeader && varyHeader.length && this.isCacheableResource(resource) && this.freshnessLifetimeGreaterThan(resource, 0);
580 _oneMonthExpirationCheck: function(resource)
582 return this.isCacheableResource(resource) &&
583 !this.hasResponseHeader(resource, "Set-Cookie") &&
584 !this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
585 this.freshnessLifetimeGreaterThan(resource, 0);
588 _oneYearExpirationCheck: function(resource)
590 return this.isCacheableResource(resource) &&
591 !this.hasResponseHeader(resource, "Set-Cookie") &&
592 !this.freshnessLifetimeGreaterThan(resource, 11 * WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
593 this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth);
597 WebInspector.AuditRules.BrowserCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype;
600 WebInspector.AuditRules.ProxyCacheControlRule = function() {
601 WebInspector.AuditRules.CacheControlRule.call(this, "http-proxycache", "Leverage proxy caching");
604 WebInspector.AuditRules.ProxyCacheControlRule.prototype = {
605 runChecks: function(resources, result, callback)
607 this.execCheck("Resources with a \"?\" in the URL are not cached by most proxy caching servers:",
608 this._questionMarkCheck, resources, result);
609 this.execCheck("Consider adding a \"Cache-Control: public\" header to the following resources:",
610 this._publicCachingCheck, resources, result);
611 this.execCheck("The following publicly cacheable resources contain a Set-Cookie header. This security vulnerability can cause cookies to be shared by multiple users.",
612 this._setCookieCacheableCheck, resources, result);
615 _questionMarkCheck: function(resource)
617 return resource.url.indexOf("?") >= 0 && !this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource);
620 _publicCachingCheck: function(resource)
622 return this.isCacheableResource(resource) &&
623 !this.isCompressible(resource) &&
624 !this.responseHeaderMatch(resource, "Cache-Control", "public") &&
625 !this.hasResponseHeader(resource, "Set-Cookie");
628 _setCookieCacheableCheck: function(resource)
630 return this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource);
634 WebInspector.AuditRules.ProxyCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype;
637 WebInspector.AuditRules.ImageDimensionsRule = function()
639 WebInspector.AuditRule.call(this, "page-imagedims", "Specify image dimensions");
642 WebInspector.AuditRules.ImageDimensionsRule.prototype = {
643 doRun: function(resources, result, callback)
645 var urlToNoDimensionCount = {};
647 function doneCallback()
649 for (var url in urlToNoDimensionCount) {
650 var entry = entry || result.addChild("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:", true);
651 var value = WebInspector.AuditRuleResult.linkifyDisplayName(url);
652 if (urlToNoDimensionCount[url] > 1)
653 value += String.sprintf(" (%d uses)", urlToNoDimensionCount[url]);
654 entry.addChild(value);
655 result.violationCount++;
657 callback(entry ? result : null);
660 function imageStylesReady(imageId, lastCall, styles)
662 const node = WebInspector.domAgent.nodeForId(imageId);
663 var src = node.getAttribute("src");
664 if (!src.asParsedURL()) {
665 for (var frameOwnerCandidate = node; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) {
666 if (frameOwnerCandidate.documentURL) {
667 var completeSrc = WebInspector.completeURL(frameOwnerCandidate.documentURL, src);
675 const computedStyle = styles.computedStyle;
676 if (computedStyle.getPropertyValue("position") === "absolute") {
682 var widthFound = "width" in styles.styleAttributes;
683 var heightFound = "height" in styles.styleAttributes;
685 var inlineStyle = styles.inlineStyle;
687 if (inlineStyle.getPropertyValue("width") !== "")
689 if (inlineStyle.getPropertyValue("height") !== "")
693 for (var i = styles.matchedCSSRules.length - 1; i >= 0 && !(widthFound && heightFound); --i) {
694 var style = styles.matchedCSSRules[i].style;
695 if (style.getPropertyValue("width") !== "")
697 if (style.getPropertyValue("height") !== "")
701 if (!widthFound || !heightFound) {
702 if (src in urlToNoDimensionCount)
703 ++urlToNoDimensionCount[src];
705 urlToNoDimensionCount[src] = 1;
712 function getStyles(nodeIds)
714 for (var i = 0; nodeIds && i < nodeIds.length; ++i)
715 WebInspector.cssModel.getStylesAsync(nodeIds[i], undefined, imageStylesReady.bind(this, nodeIds[i], i === nodeIds.length - 1));
718 function onDocumentAvailable(root)
720 WebInspector.domAgent.querySelectorAll(root.id, "img[src]", getStyles);
723 WebInspector.domAgent.requestDocument(onDocumentAvailable);
727 WebInspector.AuditRules.ImageDimensionsRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
730 WebInspector.AuditRules.CssInHeadRule = function()
732 WebInspector.AuditRule.call(this, "page-cssinhead", "Put CSS in the document head");
735 WebInspector.AuditRules.CssInHeadRule.prototype = {
736 doRun: function(resources, result, callback)
738 function evalCallback(evalResult)
741 return callback(null);
743 var summary = result.addChild("");
745 var outputMessages = [];
746 for (var url in evalResult) {
747 var urlViolations = evalResult[url];
748 if (urlViolations[0]) {
749 result.addChild(String.sprintf("%s style block(s) in the %s body should be moved to the document head.", urlViolations[0], WebInspector.AuditRuleResult.linkifyDisplayName(url)));
750 result.violationCount += urlViolations[0];
752 for (var i = 0; i < urlViolations[1].length; ++i)
753 result.addChild(String.sprintf("Link node %s should be moved to the document head in %s", WebInspector.AuditRuleResult.linkifyDisplayName(urlViolations[1][i]), WebInspector.AuditRuleResult.linkifyDisplayName(url)));
754 result.violationCount += urlViolations[1].length;
756 summary.value = String.sprintf("CSS in the document body adversely impacts rendering performance.");
760 function externalStylesheetsReceived(root, inlineStyleNodeIds, nodeIds)
764 var externalStylesheetNodeIds = nodeIds;
766 if (inlineStyleNodeIds.length || externalStylesheetNodeIds.length) {
767 var urlToViolationsArray = {};
768 var externalStylesheetHrefs = [];
769 for (var j = 0; j < externalStylesheetNodeIds.length; ++j) {
770 var linkNode = WebInspector.domAgent.nodeForId(externalStylesheetNodeIds[j]);
771 var completeHref = WebInspector.completeURL(linkNode.ownerDocument.documentURL, linkNode.getAttribute("href"));
772 externalStylesheetHrefs.push(completeHref || "<empty>");
774 urlToViolationsArray[root.documentURL] = [inlineStyleNodeIds.length, externalStylesheetHrefs];
775 result = urlToViolationsArray;
777 evalCallback(result);
780 function inlineStylesReceived(root, nodeIds)
784 WebInspector.domAgent.querySelectorAll(root.id, "body link[rel~='stylesheet'][href]", externalStylesheetsReceived.bind(null, root, nodeIds));
787 function onDocumentAvailable(root)
789 WebInspector.domAgent.querySelectorAll(root.id, "body style", inlineStylesReceived.bind(null, root));
792 WebInspector.domAgent.requestDocument(onDocumentAvailable);
796 WebInspector.AuditRules.CssInHeadRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
799 WebInspector.AuditRules.StylesScriptsOrderRule = function()
801 WebInspector.AuditRule.call(this, "page-stylescriptorder", "Optimize the order of styles and scripts");
804 WebInspector.AuditRules.StylesScriptsOrderRule.prototype = {
805 doRun: function(resources, result, callback)
807 function evalCallback(resultValue)
810 return callback(null);
812 var lateCssUrls = resultValue[0];
813 var cssBeforeInlineCount = resultValue[1];
815 var entry = result.addChild("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.", true);
816 entry.addURLs(lateCssUrls);
817 result.violationCount += lateCssUrls.length;
819 if (cssBeforeInlineCount) {
820 result.addChild(String.sprintf(" %d inline script block%s 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.", cssBeforeInlineCount, cssBeforeInlineCount > 1 ? "s were" : " was"));
821 result.violationCount += cssBeforeInlineCount;
826 function cssBeforeInlineReceived(lateStyleIds, nodeIds)
831 var cssBeforeInlineCount = nodeIds.length;
833 if (lateStyleIds.length || cssBeforeInlineCount) {
834 var lateStyleUrls = [];
835 for (var i = 0; i < lateStyleIds.length; ++i) {
836 var lateStyleNode = WebInspector.domAgent.nodeForId(lateStyleIds[i]);
837 var completeHref = WebInspector.completeURL(lateStyleNode.ownerDocument.documentURL, lateStyleNode.getAttribute("href"));
838 lateStyleUrls.push(completeHref || "<empty>");
840 result = [ lateStyleUrls, cssBeforeInlineCount ];
843 evalCallback(result);
846 function lateStylesReceived(root, nodeIds)
851 WebInspector.domAgent.querySelectorAll(root.id, "head link[rel~='stylesheet'][href] ~ script:not([src])", cssBeforeInlineReceived.bind(null, nodeIds));
854 function onDocumentAvailable(root)
856 WebInspector.domAgent.querySelectorAll(root.id, "head script[src] ~ link[rel~='stylesheet'][href]", lateStylesReceived.bind(null, root));
859 WebInspector.domAgent.requestDocument(onDocumentAvailable);
863 WebInspector.AuditRules.StylesScriptsOrderRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
866 WebInspector.AuditRules.CookieRuleBase = function(id, name)
868 WebInspector.AuditRule.call(this, id, name);
871 WebInspector.AuditRules.CookieRuleBase.prototype = {
872 doRun: function(resources, result, callback)
875 function resultCallback(receivedCookies, isAdvanced) {
876 self.processCookies(isAdvanced ? receivedCookies : [], resources, result);
879 WebInspector.Cookies.getCookiesAsync(resultCallback);
882 mapResourceCookies: function(resourcesByDomain, allCookies, callback)
884 for (var i = 0; i < allCookies.length; ++i) {
885 for (var resourceDomain in resourcesByDomain) {
886 if (WebInspector.Cookies.cookieDomainMatchesResourceDomain(allCookies[i].domain, resourceDomain))
887 this._callbackForResourceCookiePairs(resourcesByDomain[resourceDomain], allCookies[i], callback);
892 _callbackForResourceCookiePairs: function(resources, cookie, callback)
896 for (var i = 0; i < resources.length; ++i) {
897 if (WebInspector.Cookies.cookieMatchesResourceURL(cookie, resources[i].url))
898 callback(resources[i], cookie);
903 WebInspector.AuditRules.CookieRuleBase.prototype.__proto__ = WebInspector.AuditRule.prototype;
906 WebInspector.AuditRules.CookieSizeRule = function(avgBytesThreshold)
908 WebInspector.AuditRules.CookieRuleBase.call(this, "http-cookiesize", "Minimize cookie size");
909 this._avgBytesThreshold = avgBytesThreshold;
910 this._maxBytesThreshold = 1000;
913 WebInspector.AuditRules.CookieSizeRule.prototype = {
914 _average: function(cookieArray)
917 for (var i = 0; i < cookieArray.length; ++i)
918 total += cookieArray[i].size;
919 return cookieArray.length ? Math.round(total / cookieArray.length) : 0;
922 _max: function(cookieArray)
925 for (var i = 0; i < cookieArray.length; ++i)
926 result = Math.max(cookieArray[i].size, result);
930 processCookies: function(allCookies, resources, result)
932 function maxSizeSorter(a, b)
934 return b.maxCookieSize - a.maxCookieSize;
937 function avgSizeSorter(a, b)
939 return b.avgCookieSize - a.avgCookieSize;
942 var cookiesPerResourceDomain = {};
944 function collectorCallback(resource, cookie)
946 var cookies = cookiesPerResourceDomain[resource.domain];
949 cookiesPerResourceDomain[resource.domain] = cookies;
951 cookies.push(cookie);
954 if (!allCookies.length)
957 var sortedCookieSizes = [];
959 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources,
962 var matchingResourceData = {};
963 this.mapResourceCookies(domainToResourcesMap, allCookies, collectorCallback.bind(this));
965 for (var resourceDomain in cookiesPerResourceDomain) {
966 var cookies = cookiesPerResourceDomain[resourceDomain];
967 sortedCookieSizes.push({
968 domain: resourceDomain,
969 avgCookieSize: this._average(cookies),
970 maxCookieSize: this._max(cookies)
973 var avgAllCookiesSize = this._average(allCookies);
975 var hugeCookieDomains = [];
976 sortedCookieSizes.sort(maxSizeSorter);
978 for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
979 var maxCookieSize = sortedCookieSizes[i].maxCookieSize;
980 if (maxCookieSize > this._maxBytesThreshold)
981 hugeCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(sortedCookieSizes[i].domain) + ": " + Number.bytesToString(maxCookieSize));
984 var bigAvgCookieDomains = [];
985 sortedCookieSizes.sort(avgSizeSorter);
986 for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
987 var domain = sortedCookieSizes[i].domain;
988 var avgCookieSize = sortedCookieSizes[i].avgCookieSize;
989 if (avgCookieSize > this._avgBytesThreshold && avgCookieSize < this._maxBytesThreshold)
990 bigAvgCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(domain) + ": " + Number.bytesToString(avgCookieSize));
992 result.addChild(String.sprintf("The average cookie size for all requests on this page is %s", Number.bytesToString(avgAllCookiesSize)));
995 if (hugeCookieDomains.length) {
996 var entry = result.addChild("The following domains have a cookie size in excess of 1KB. This is harmful because requests with cookies larger than 1KB typically cannot fit into a single network packet.", true);
997 entry.addURLs(hugeCookieDomains);
998 result.violationCount += hugeCookieDomains.length;
1001 if (bigAvgCookieDomains.length) {
1002 var entry = result.addChild(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.", this._avgBytesThreshold), true);
1003 entry.addURLs(bigAvgCookieDomains);
1004 result.violationCount += bigAvgCookieDomains.length;
1009 WebInspector.AuditRules.CookieSizeRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype;
1012 WebInspector.AuditRules.StaticCookielessRule = function(minResources)
1014 WebInspector.AuditRules.CookieRuleBase.call(this, "http-staticcookieless", "Serve static content from a cookieless domain");
1015 this._minResources = minResources;
1018 WebInspector.AuditRules.StaticCookielessRule.prototype = {
1019 processCookies: function(allCookies, resources, result)
1021 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources,
1022 [WebInspector.Resource.Type.Stylesheet,
1023 WebInspector.Resource.Type.Image],
1025 var totalStaticResources = 0;
1026 for (var domain in domainToResourcesMap)
1027 totalStaticResources += domainToResourcesMap[domain].length;
1028 if (totalStaticResources < this._minResources)
1030 var matchingResourceData = {};
1031 this.mapResourceCookies(domainToResourcesMap, allCookies, this._collectorCallback.bind(this, matchingResourceData));
1034 var cookieBytes = 0;
1035 for (var url in matchingResourceData) {
1037 cookieBytes += matchingResourceData[url]
1039 if (badUrls.length < this._minResources)
1042 var entry = result.addChild(String.sprintf("%s of cookies were sent with the following static resources. Serve these static resources from a domain that does not set cookies:", Number.bytesToString(cookieBytes)), true);
1043 entry.addURLs(badUrls);
1044 result.violationCount = badUrls.length;
1047 _collectorCallback: function(matchingResourceData, resource, cookie)
1049 matchingResourceData[resource.url] = (matchingResourceData[resource.url] || 0) + cookie.size;
1053 WebInspector.AuditRules.StaticCookielessRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype;