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;
68 * @extends {WebInspector.AuditRule}
70 WebInspector.AuditRules.GzipRule = function()
72 WebInspector.AuditRule.call(this, "network-gzip", "Enable gzip compression");
75 WebInspector.AuditRules.GzipRule.prototype = {
76 doRun: function(resources, result, callback)
79 var compressedSize = 0;
80 var candidateSize = 0;
81 var summary = result.addChild("", true);
82 for (var i = 0, length = resources.length; i < length; ++i) {
83 var resource = resources[i];
84 if (resource.statusCode === 304)
85 continue; // Do not test 304 Not Modified resources as their contents are always empty.
86 if (this._shouldCompress(resource)) {
87 var size = resource.resourceSize;
88 candidateSize += size;
89 if (this._isCompressed(resource)) {
90 compressedSize += size;
93 var savings = 2 * size / 3;
94 totalSavings += savings;
95 summary.addFormatted("%r could save ~%s", resource.url, Number.bytesToString(savings));
96 result.violationCount++;
100 return callback(null);
101 summary.value = String.sprintf("Compressing the following resources with gzip could reduce their transfer size by about two thirds (~%s):", Number.bytesToString(totalSavings));
105 _isCompressed: function(resource)
107 var encodingHeader = resource.responseHeaders["Content-Encoding"];
111 return /\b(?:gzip|deflate)\b/.test(encodingHeader);
114 _shouldCompress: function(resource)
116 return WebInspector.Resource.Type.isTextType(resource.type) && resource.domain && resource.resourceSize !== undefined && resource.resourceSize > 150;
120 WebInspector.AuditRules.GzipRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
124 * @extends {WebInspector.AuditRule}
126 WebInspector.AuditRules.CombineExternalResourcesRule = function(id, name, type, resourceTypeName, allowedPerDomain)
128 WebInspector.AuditRule.call(this, id, name);
130 this._resourceTypeName = resourceTypeName;
131 this._allowedPerDomain = allowedPerDomain;
134 WebInspector.AuditRules.CombineExternalResourcesRule.prototype = {
135 doRun: function(resources, result, callback)
137 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, [this._type], false);
138 var penalizedResourceCount = 0;
139 // TODO: refactor according to the chosen i18n approach
140 var summary = result.addChild("", true);
141 for (var domain in domainToResourcesMap) {
142 var domainResources = domainToResourcesMap[domain];
143 var extraResourceCount = domainResources.length - this._allowedPerDomain;
144 if (extraResourceCount <= 0)
146 penalizedResourceCount += extraResourceCount - 1;
147 summary.addChild(String.sprintf("%d %s resources served from %s.", domainResources.length, this._resourceTypeName, WebInspector.AuditRuleResult.resourceDomain(domain)));
148 result.violationCount += domainResources.length;
150 if (!penalizedResourceCount)
151 return callback(null);
153 summary.value = "There are multiple resources served from same domain. Consider combining them into as few files as possible.";
158 WebInspector.AuditRules.CombineExternalResourcesRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
162 * @extends {WebInspector.AuditRules.CombineExternalResourcesRule}
164 WebInspector.AuditRules.CombineJsResourcesRule = function(allowedPerDomain) {
165 WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externaljs", "Combine external JavaScript", WebInspector.Resource.Type.Script, "JavaScript", allowedPerDomain);
168 WebInspector.AuditRules.CombineJsResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype;
172 * @extends {WebInspector.AuditRules.CombineExternalResourcesRule}
174 WebInspector.AuditRules.CombineCssResourcesRule = function(allowedPerDomain) {
175 WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externalcss", "Combine external CSS", WebInspector.Resource.Type.Stylesheet, "CSS", allowedPerDomain);
178 WebInspector.AuditRules.CombineCssResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype;
182 * @extends {WebInspector.AuditRule}
184 WebInspector.AuditRules.MinimizeDnsLookupsRule = function(hostCountThreshold) {
185 WebInspector.AuditRule.call(this, "network-minimizelookups", "Minimize DNS lookups");
186 this._hostCountThreshold = hostCountThreshold;
189 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype = {
190 doRun: function(resources, result, callback)
192 var summary = result.addChild("");
193 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, undefined, false);
194 for (var domain in domainToResourcesMap) {
195 if (domainToResourcesMap[domain].length > 1)
197 var parsedURL = domain.asParsedURL();
200 if (!parsedURL.host.search(WebInspector.AuditRules.IPAddressRegexp))
201 continue; // an IP address
202 summary.addSnippet(domain);
203 result.violationCount++;
205 if (!summary.children || summary.children.length <= this._hostCountThreshold)
206 return callback(null);
208 summary.value = "The following domains only serve one resource each. If possible, avoid the extra DNS lookups by serving these resources from existing domains.";
213 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
217 * @extends {WebInspector.AuditRule}
219 WebInspector.AuditRules.ParallelizeDownloadRule = function(optimalHostnameCount, minRequestThreshold, minBalanceThreshold)
221 WebInspector.AuditRule.call(this, "network-parallelizehosts", "Parallelize downloads across hostnames");
222 this._optimalHostnameCount = optimalHostnameCount;
223 this._minRequestThreshold = minRequestThreshold;
224 this._minBalanceThreshold = minBalanceThreshold;
227 WebInspector.AuditRules.ParallelizeDownloadRule.prototype = {
228 doRun: function(resources, result, callback)
230 function hostSorter(a, b)
232 var aCount = domainToResourcesMap[a].length;
233 var bCount = domainToResourcesMap[b].length;
234 return (aCount < bCount) ? 1 : (aCount == bCount) ? 0 : -1;
237 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(
239 [WebInspector.Resource.Type.Stylesheet, WebInspector.Resource.Type.Image],
243 for (var url in domainToResourcesMap)
247 return callback(null); // no hosts (local file or something)
249 hosts.sort(hostSorter);
251 var optimalHostnameCount = this._optimalHostnameCount;
252 if (hosts.length > optimalHostnameCount)
253 hosts.splice(optimalHostnameCount);
255 var busiestHostResourceCount = domainToResourcesMap[hosts[0]].length;
256 var resourceCountAboveThreshold = busiestHostResourceCount - this._minRequestThreshold;
257 if (resourceCountAboveThreshold <= 0)
258 return callback(null);
260 var avgResourcesPerHost = 0;
261 for (var i = 0, size = hosts.length; i < size; ++i)
262 avgResourcesPerHost += domainToResourcesMap[hosts[i]].length;
264 // Assume optimal parallelization.
265 avgResourcesPerHost /= optimalHostnameCount;
266 avgResourcesPerHost = Math.max(avgResourcesPerHost, 1);
268 var pctAboveAvg = (resourceCountAboveThreshold / avgResourcesPerHost) - 1.0;
269 var minBalanceThreshold = this._minBalanceThreshold;
270 if (pctAboveAvg < minBalanceThreshold)
271 return callback(null);
273 var resourcesOnBusiestHost = domainToResourcesMap[hosts[0]];
274 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);
275 for (var i = 0; i < resourcesOnBusiestHost.length; ++i)
276 entry.addURL(resourcesOnBusiestHost[i].url);
278 result.violationCount = resourcesOnBusiestHost.length;
283 WebInspector.AuditRules.ParallelizeDownloadRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
286 * The reported CSS rule size is incorrect (parsed != original in WebKit),
287 * so use percentages instead, which gives a better approximation.
289 * @extends {WebInspector.AuditRule}
291 WebInspector.AuditRules.UnusedCssRule = function()
293 WebInspector.AuditRule.call(this, "page-unusedcss", "Remove unused CSS rules");
296 WebInspector.AuditRules.UnusedCssRule.prototype = {
297 doRun: function(resources, result, callback)
301 function evalCallback(styleSheets) {
302 if (!styleSheets.length)
303 return callback(null);
305 var pseudoSelectorRegexp = /:hover|:link|:active|:visited|:focus|:before|:after/;
307 var testedSelectors = {};
308 for (var i = 0; i < styleSheets.length; ++i) {
309 var styleSheet = styleSheets[i];
310 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
311 var selectorText = styleSheet.rules[curRule].selectorText;
312 if (selectorText.match(pseudoSelectorRegexp) || testedSelectors[selectorText])
314 selectors.push(selectorText);
315 testedSelectors[selectorText] = 1;
319 function selectorsCallback(callback, styleSheets, testedSelectors, foundSelectors)
321 var inlineBlockOrdinal = 0;
322 var totalStylesheetSize = 0;
323 var totalUnusedStylesheetSize = 0;
326 for (var i = 0; i < styleSheets.length; ++i) {
327 var styleSheet = styleSheets[i];
328 var stylesheetSize = 0;
329 var unusedStylesheetSize = 0;
330 var unusedRules = [];
331 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
332 var rule = styleSheet.rules[curRule];
333 // Exact computation whenever source ranges are available.
334 var textLength = (rule.selectorRange && rule.style.range && rule.style.range.end) ? rule.style.range.end - rule.selectorRange.start + 1 : 0;
335 if (!textLength && rule.style.cssText)
336 textLength = rule.style.cssText.length + rule.selectorText.length;
337 stylesheetSize += textLength;
338 if (!testedSelectors[rule.selectorText] || foundSelectors[rule.selectorText])
340 unusedStylesheetSize += textLength;
341 unusedRules.push(rule.selectorText);
343 totalStylesheetSize += stylesheetSize;
344 totalUnusedStylesheetSize += unusedStylesheetSize;
346 if (!unusedRules.length)
349 var resource = WebInspector.resourceForURL(styleSheet.sourceURL);
350 var isInlineBlock = resource && resource.type == WebInspector.Resource.Type.Document;
351 var url = !isInlineBlock ? WebInspector.AuditRuleResult.linkifyDisplayName(styleSheet.sourceURL) : String.sprintf("Inline block #%d", ++inlineBlockOrdinal);
352 var pctUnused = Math.round(100 * unusedStylesheetSize / stylesheetSize);
354 summary = result.addChild("", true);
355 var entry = summary.addFormatted("%s: %s (%d%%) is not used by the current page.", url, Number.bytesToString(unusedStylesheetSize), pctUnused);
357 for (var j = 0; j < unusedRules.length; ++j)
358 entry.addSnippet(unusedRules[j]);
360 result.violationCount += unusedRules.length;
363 if (!totalUnusedStylesheetSize)
364 return callback(null);
366 var totalUnusedPercent = Math.round(100 * totalUnusedStylesheetSize / totalStylesheetSize);
367 summary.value = String.sprintf("%s (%d%%) of CSS is not used by the current page.", Number.bytesToString(totalUnusedStylesheetSize), totalUnusedPercent);
372 var foundSelectors = {};
373 function queryCallback(boundSelectorsCallback, selector, styleSheets, testedSelectors, nodeId)
376 foundSelectors[selector] = true;
377 if (boundSelectorsCallback)
378 boundSelectorsCallback(foundSelectors);
381 function documentLoaded(selectors, document) {
382 for (var i = 0; i < selectors.length; ++i)
383 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));
386 WebInspector.domAgent.requestDocument(documentLoaded.bind(null, selectors));
389 function styleSheetCallback(styleSheets, sourceURL, continuation, styleSheet)
392 styleSheet.sourceURL = sourceURL;
393 styleSheets.push(styleSheet);
396 continuation(styleSheets);
399 function allStylesCallback(error, styleSheetInfos)
401 if (error || !styleSheetInfos || !styleSheetInfos.length)
402 return evalCallback([]);
403 var styleSheets = [];
404 for (var i = 0; i < styleSheetInfos.length; ++i) {
405 var info = styleSheetInfos[i];
406 WebInspector.CSSStyleSheet.createForId(info.styleSheetId, styleSheetCallback.bind(null, styleSheets, info.sourceURL, i == styleSheetInfos.length - 1 ? evalCallback : null));
410 CSSAgent.getAllStyleSheets(allStylesCallback);
414 WebInspector.AuditRules.UnusedCssRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
418 * @extends {WebInspector.AuditRule}
420 WebInspector.AuditRules.CacheControlRule = function(id, name)
422 WebInspector.AuditRule.call(this, id, name);
425 WebInspector.AuditRules.CacheControlRule.MillisPerMonth = 1000 * 60 * 60 * 24 * 30;
427 WebInspector.AuditRules.CacheControlRule.prototype = {
429 doRun: function(resources, result, callback)
431 var cacheableAndNonCacheableResources = this._cacheableAndNonCacheableResources(resources);
432 if (cacheableAndNonCacheableResources[0].length)
433 this.runChecks(cacheableAndNonCacheableResources[0], result);
434 this.handleNonCacheableResources(cacheableAndNonCacheableResources[1], result);
439 handleNonCacheableResources: function(resources, result)
443 _cacheableAndNonCacheableResources: function(resources)
445 var processedResources = [[], []];
446 for (var i = 0; i < resources.length; ++i) {
447 var resource = resources[i];
448 if (!this.isCacheableResource(resource))
450 if (this._isExplicitlyNonCacheable(resource))
451 processedResources[1].push(resource);
453 processedResources[0].push(resource);
455 return processedResources;
458 execCheck: function(messageText, resourceCheckFunction, resources, result)
460 var resourceCount = resources.length;
462 for (var i = 0; i < resourceCount; ++i) {
463 if (resourceCheckFunction.call(this, resources[i]))
464 urls.push(resources[i].url);
467 var entry = result.addChild(messageText, true);
469 result.violationCount += urls.length;
473 freshnessLifetimeGreaterThan: function(resource, timeMs)
475 var dateHeader = this.responseHeader(resource, "Date");
479 var dateHeaderMs = Date.parse(dateHeader);
480 if (isNaN(dateHeaderMs))
483 var freshnessLifetimeMs;
484 var maxAgeMatch = this.responseHeaderMatch(resource, "Cache-Control", "max-age=(\\d+)");
487 freshnessLifetimeMs = (maxAgeMatch[1]) ? 1000 * maxAgeMatch[1] : 0;
489 var expiresHeader = this.responseHeader(resource, "Expires");
491 var expDate = Date.parse(expiresHeader);
493 freshnessLifetimeMs = expDate - dateHeaderMs;
497 return (isNaN(freshnessLifetimeMs)) ? false : freshnessLifetimeMs > timeMs;
500 responseHeader: function(resource, header)
502 return resource.responseHeaders[header];
505 hasResponseHeader: function(resource, header)
507 return resource.responseHeaders[header] !== undefined;
510 isCompressible: function(resource)
512 return WebInspector.Resource.Type.isTextType(resource.type);
515 isPubliclyCacheable: function(resource)
517 if (this._isExplicitlyNonCacheable(resource))
520 if (this.responseHeaderMatch(resource, "Cache-Control", "public"))
523 return resource.url.indexOf("?") == -1 && !this.responseHeaderMatch(resource, "Cache-Control", "private");
526 responseHeaderMatch: function(resource, header, regexp)
528 return resource.responseHeaders[header]
529 ? resource.responseHeaders[header].match(new RegExp(regexp, "im"))
533 hasExplicitExpiration: function(resource)
535 return this.hasResponseHeader(resource, "Date") &&
536 (this.hasResponseHeader(resource, "Expires") || this.responseHeaderMatch(resource, "Cache-Control", "max-age"));
539 _isExplicitlyNonCacheable: function(resource)
541 var hasExplicitExp = this.hasExplicitExpiration(resource);
542 return this.responseHeaderMatch(resource, "Cache-Control", "(no-cache|no-store|must-revalidate)") ||
543 this.responseHeaderMatch(resource, "Pragma", "no-cache") ||
544 (hasExplicitExp && !this.freshnessLifetimeGreaterThan(resource, 0)) ||
545 (!hasExplicitExp && resource.url && resource.url.indexOf("?") >= 0) ||
546 (!hasExplicitExp && !this.isCacheableResource(resource));
549 isCacheableResource: function(resource)
551 return resource.statusCode !== undefined && WebInspector.AuditRules.CacheableResponseCodes[resource.statusCode];
555 WebInspector.AuditRules.CacheControlRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
559 * @extends {WebInspector.AuditRules.CacheControlRule}
561 WebInspector.AuditRules.BrowserCacheControlRule = function()
563 WebInspector.AuditRules.CacheControlRule.call(this, "http-browsercache", "Leverage browser caching");
566 WebInspector.AuditRules.BrowserCacheControlRule.prototype = {
567 handleNonCacheableResources: function(resources, result)
569 if (resources.length) {
570 var entry = result.addChild("The following resources are explicitly non-cacheable. Consider making them cacheable if possible:", true);
571 result.violationCount += resources.length;
572 for (var i = 0; i < resources.length; ++i)
573 entry.addURL(resources[i].url);
577 runChecks: function(resources, result, callback)
579 this.execCheck("The following resources are missing a cache expiration. Resources that do not specify an expiration may not be cached by browsers:",
580 this._missingExpirationCheck, resources, result);
581 this.execCheck("The following resources specify a \"Vary\" header that disables caching in most versions of Internet Explorer:",
582 this._varyCheck, resources, result);
583 this.execCheck("The following cacheable resources have a short freshness lifetime:",
584 this._oneMonthExpirationCheck, resources, result);
586 // Unable to implement the favicon check due to the WebKit limitations.
587 this.execCheck("To further improve cache hit rate, specify an expiration one year in the future for the following cacheable resources:",
588 this._oneYearExpirationCheck, resources, result);
591 _missingExpirationCheck: function(resource)
593 return this.isCacheableResource(resource) && !this.hasResponseHeader(resource, "Set-Cookie") && !this.hasExplicitExpiration(resource);
596 _varyCheck: function(resource)
598 var varyHeader = this.responseHeader(resource, "Vary");
600 varyHeader = varyHeader.replace(/User-Agent/gi, "");
601 varyHeader = varyHeader.replace(/Accept-Encoding/gi, "");
602 varyHeader = varyHeader.replace(/[, ]*/g, "");
604 return varyHeader && varyHeader.length && this.isCacheableResource(resource) && this.freshnessLifetimeGreaterThan(resource, 0);
607 _oneMonthExpirationCheck: function(resource)
609 return this.isCacheableResource(resource) &&
610 !this.hasResponseHeader(resource, "Set-Cookie") &&
611 !this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
612 this.freshnessLifetimeGreaterThan(resource, 0);
615 _oneYearExpirationCheck: function(resource)
617 return this.isCacheableResource(resource) &&
618 !this.hasResponseHeader(resource, "Set-Cookie") &&
619 !this.freshnessLifetimeGreaterThan(resource, 11 * WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
620 this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth);
624 WebInspector.AuditRules.BrowserCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype;
628 * @extends {WebInspector.AuditRules.CacheControlRule}
630 WebInspector.AuditRules.ProxyCacheControlRule = function() {
631 WebInspector.AuditRules.CacheControlRule.call(this, "http-proxycache", "Leverage proxy caching");
634 WebInspector.AuditRules.ProxyCacheControlRule.prototype = {
635 runChecks: function(resources, result, callback)
637 this.execCheck("Resources with a \"?\" in the URL are not cached by most proxy caching servers:",
638 this._questionMarkCheck, resources, result);
639 this.execCheck("Consider adding a \"Cache-Control: public\" header to the following resources:",
640 this._publicCachingCheck, resources, result);
641 this.execCheck("The following publicly cacheable resources contain a Set-Cookie header. This security vulnerability can cause cookies to be shared by multiple users.",
642 this._setCookieCacheableCheck, resources, result);
645 _questionMarkCheck: function(resource)
647 return resource.url.indexOf("?") >= 0 && !this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource);
650 _publicCachingCheck: function(resource)
652 return this.isCacheableResource(resource) &&
653 !this.isCompressible(resource) &&
654 !this.responseHeaderMatch(resource, "Cache-Control", "public") &&
655 !this.hasResponseHeader(resource, "Set-Cookie");
658 _setCookieCacheableCheck: function(resource)
660 return this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource);
664 WebInspector.AuditRules.ProxyCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype;
668 * @extends {WebInspector.AuditRule}
670 WebInspector.AuditRules.ImageDimensionsRule = function()
672 WebInspector.AuditRule.call(this, "page-imagedims", "Specify image dimensions");
675 WebInspector.AuditRules.ImageDimensionsRule.prototype = {
676 doRun: function(resources, result, callback)
678 var urlToNoDimensionCount = {};
680 function doneCallback()
682 for (var url in urlToNoDimensionCount) {
683 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);
685 if (urlToNoDimensionCount[url] > 1)
686 format += " (%d uses)";
687 entry.addFormatted(format, url, urlToNoDimensionCount[url]);
688 result.violationCount++;
690 callback(entry ? result : null);
693 function imageStylesReady(imageId, styles, isLastStyle, computedStyle)
695 const node = WebInspector.domAgent.nodeForId(imageId);
696 var src = node.getAttribute("src");
697 if (!src.asParsedURL()) {
698 for (var frameOwnerCandidate = node; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) {
699 if (frameOwnerCandidate.documentURL) {
700 var completeSrc = WebInspector.completeURL(frameOwnerCandidate.documentURL, src);
708 if (computedStyle.getPropertyValue("position") === "absolute") {
714 var widthFound = "width" in styles.styleAttributes;
715 var heightFound = "height" in styles.styleAttributes;
717 var inlineStyle = styles.inlineStyle;
719 if (inlineStyle.getPropertyValue("width") !== "")
721 if (inlineStyle.getPropertyValue("height") !== "")
725 for (var i = styles.matchedCSSRules.length - 1; i >= 0 && !(widthFound && heightFound); --i) {
726 var style = styles.matchedCSSRules[i].style;
727 if (style.getPropertyValue("width") !== "")
729 if (style.getPropertyValue("height") !== "")
733 if (!widthFound || !heightFound) {
734 if (src in urlToNoDimensionCount)
735 ++urlToNoDimensionCount[src];
737 urlToNoDimensionCount[src] = 1;
744 function getStyles(nodeIds)
746 var targetResult = {};
748 function inlineCallback(inlineStyle, styleAttributes)
750 targetResult.inlineStyle = inlineStyle;
751 targetResult.styleAttributes = styleAttributes;
754 function matchedCallback(result)
757 targetResult.matchedCSSRules = result.matchedCSSRules;
760 if (!nodeIds || !nodeIds.length)
763 for (var i = 0; nodeIds && i < nodeIds.length; ++i) {
764 WebInspector.cssModel.getMatchedStylesAsync(nodeIds[i], undefined, false, false, matchedCallback);
765 WebInspector.cssModel.getInlineStylesAsync(nodeIds[i], inlineCallback);
766 WebInspector.cssModel.getComputedStyleAsync(nodeIds[i], undefined, imageStylesReady.bind(null, nodeIds[i], targetResult, i === nodeIds.length - 1));
770 function onDocumentAvailable(root)
772 WebInspector.domAgent.querySelectorAll(root.id, "img[src]", getStyles);
775 WebInspector.domAgent.requestDocument(onDocumentAvailable);
779 WebInspector.AuditRules.ImageDimensionsRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
783 * @extends {WebInspector.AuditRule}
785 WebInspector.AuditRules.CssInHeadRule = function()
787 WebInspector.AuditRule.call(this, "page-cssinhead", "Put CSS in the document head");
790 WebInspector.AuditRules.CssInHeadRule.prototype = {
791 doRun: function(resources, result, callback)
793 function evalCallback(evalResult)
796 return callback(null);
798 var summary = result.addChild("");
800 var outputMessages = [];
801 for (var url in evalResult) {
802 var urlViolations = evalResult[url];
803 if (urlViolations[0]) {
804 result.addFormatted("%s style block(s) in the %r body should be moved to the document head.", urlViolations[0], url);
805 result.violationCount += urlViolations[0];
807 for (var i = 0; i < urlViolations[1].length; ++i)
808 result.addFormatted("Link node %r should be moved to the document head in %r", urlViolations[1][i], url);
809 result.violationCount += urlViolations[1].length;
811 summary.value = String.sprintf("CSS in the document body adversely impacts rendering performance.");
815 function externalStylesheetsReceived(root, inlineStyleNodeIds, nodeIds)
819 var externalStylesheetNodeIds = nodeIds;
821 if (inlineStyleNodeIds.length || externalStylesheetNodeIds.length) {
822 var urlToViolationsArray = {};
823 var externalStylesheetHrefs = [];
824 for (var j = 0; j < externalStylesheetNodeIds.length; ++j) {
825 var linkNode = WebInspector.domAgent.nodeForId(externalStylesheetNodeIds[j]);
826 var completeHref = WebInspector.completeURL(linkNode.ownerDocument.documentURL, linkNode.getAttribute("href"));
827 externalStylesheetHrefs.push(completeHref || "<empty>");
829 urlToViolationsArray[root.documentURL] = [inlineStyleNodeIds.length, externalStylesheetHrefs];
830 result = urlToViolationsArray;
832 evalCallback(result);
835 function inlineStylesReceived(root, nodeIds)
839 WebInspector.domAgent.querySelectorAll(root.id, "body link[rel~='stylesheet'][href]", externalStylesheetsReceived.bind(null, root, nodeIds));
842 function onDocumentAvailable(root)
844 WebInspector.domAgent.querySelectorAll(root.id, "body style", inlineStylesReceived.bind(null, root));
847 WebInspector.domAgent.requestDocument(onDocumentAvailable);
851 WebInspector.AuditRules.CssInHeadRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
855 * @extends {WebInspector.AuditRule}
857 WebInspector.AuditRules.StylesScriptsOrderRule = function()
859 WebInspector.AuditRule.call(this, "page-stylescriptorder", "Optimize the order of styles and scripts");
862 WebInspector.AuditRules.StylesScriptsOrderRule.prototype = {
863 doRun: function(resources, result, callback)
865 function evalCallback(resultValue)
868 return callback(null);
870 var lateCssUrls = resultValue[0];
871 var cssBeforeInlineCount = resultValue[1];
873 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);
874 entry.addURLs(lateCssUrls);
875 result.violationCount += lateCssUrls.length;
877 if (cssBeforeInlineCount) {
878 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"));
879 result.violationCount += cssBeforeInlineCount;
884 function cssBeforeInlineReceived(lateStyleIds, nodeIds)
889 var cssBeforeInlineCount = nodeIds.length;
891 if (lateStyleIds.length || cssBeforeInlineCount) {
892 var lateStyleUrls = [];
893 for (var i = 0; i < lateStyleIds.length; ++i) {
894 var lateStyleNode = WebInspector.domAgent.nodeForId(lateStyleIds[i]);
895 var completeHref = WebInspector.completeURL(lateStyleNode.ownerDocument.documentURL, lateStyleNode.getAttribute("href"));
896 lateStyleUrls.push(completeHref || "<empty>");
898 result = [ lateStyleUrls, cssBeforeInlineCount ];
901 evalCallback(result);
904 function lateStylesReceived(root, nodeIds)
909 WebInspector.domAgent.querySelectorAll(root.id, "head link[rel~='stylesheet'][href] ~ script:not([src])", cssBeforeInlineReceived.bind(null, nodeIds));
912 function onDocumentAvailable(root)
914 WebInspector.domAgent.querySelectorAll(root.id, "head script[src] ~ link[rel~='stylesheet'][href]", lateStylesReceived.bind(null, root));
917 WebInspector.domAgent.requestDocument(onDocumentAvailable);
921 WebInspector.AuditRules.StylesScriptsOrderRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
925 * @extends {WebInspector.AuditRule}
927 WebInspector.AuditRules.CookieRuleBase = function(id, name)
929 WebInspector.AuditRule.call(this, id, name);
932 WebInspector.AuditRules.CookieRuleBase.prototype = {
933 doRun: function(resources, result, callback)
936 function resultCallback(receivedCookies, isAdvanced) {
937 self.processCookies(isAdvanced ? receivedCookies : [], resources, result);
940 WebInspector.Cookies.getCookiesAsync(resultCallback);
943 mapResourceCookies: function(resourcesByDomain, allCookies, callback)
945 for (var i = 0; i < allCookies.length; ++i) {
946 for (var resourceDomain in resourcesByDomain) {
947 if (WebInspector.Cookies.cookieDomainMatchesResourceDomain(allCookies[i].domain, resourceDomain))
948 this._callbackForResourceCookiePairs(resourcesByDomain[resourceDomain], allCookies[i], callback);
953 _callbackForResourceCookiePairs: function(resources, cookie, callback)
957 for (var i = 0; i < resources.length; ++i) {
958 if (WebInspector.Cookies.cookieMatchesResourceURL(cookie, resources[i].url))
959 callback(resources[i], cookie);
964 WebInspector.AuditRules.CookieRuleBase.prototype.__proto__ = WebInspector.AuditRule.prototype;
968 * @extends {WebInspector.AuditRules.CookieRuleBase}
970 WebInspector.AuditRules.CookieSizeRule = function(avgBytesThreshold)
972 WebInspector.AuditRules.CookieRuleBase.call(this, "http-cookiesize", "Minimize cookie size");
973 this._avgBytesThreshold = avgBytesThreshold;
974 this._maxBytesThreshold = 1000;
977 WebInspector.AuditRules.CookieSizeRule.prototype = {
978 _average: function(cookieArray)
981 for (var i = 0; i < cookieArray.length; ++i)
982 total += cookieArray[i].size;
983 return cookieArray.length ? Math.round(total / cookieArray.length) : 0;
986 _max: function(cookieArray)
989 for (var i = 0; i < cookieArray.length; ++i)
990 result = Math.max(cookieArray[i].size, result);
994 processCookies: function(allCookies, resources, result)
996 function maxSizeSorter(a, b)
998 return b.maxCookieSize - a.maxCookieSize;
1001 function avgSizeSorter(a, b)
1003 return b.avgCookieSize - a.avgCookieSize;
1006 var cookiesPerResourceDomain = {};
1008 function collectorCallback(resource, cookie)
1010 var cookies = cookiesPerResourceDomain[resource.domain];
1013 cookiesPerResourceDomain[resource.domain] = cookies;
1015 cookies.push(cookie);
1018 if (!allCookies.length)
1021 var sortedCookieSizes = [];
1023 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources,
1026 var matchingResourceData = {};
1027 this.mapResourceCookies(domainToResourcesMap, allCookies, collectorCallback.bind(this));
1029 for (var resourceDomain in cookiesPerResourceDomain) {
1030 var cookies = cookiesPerResourceDomain[resourceDomain];
1031 sortedCookieSizes.push({
1032 domain: resourceDomain,
1033 avgCookieSize: this._average(cookies),
1034 maxCookieSize: this._max(cookies)
1037 var avgAllCookiesSize = this._average(allCookies);
1039 var hugeCookieDomains = [];
1040 sortedCookieSizes.sort(maxSizeSorter);
1042 for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
1043 var maxCookieSize = sortedCookieSizes[i].maxCookieSize;
1044 if (maxCookieSize > this._maxBytesThreshold)
1045 hugeCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(sortedCookieSizes[i].domain) + ": " + Number.bytesToString(maxCookieSize));
1048 var bigAvgCookieDomains = [];
1049 sortedCookieSizes.sort(avgSizeSorter);
1050 for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
1051 var domain = sortedCookieSizes[i].domain;
1052 var avgCookieSize = sortedCookieSizes[i].avgCookieSize;
1053 if (avgCookieSize > this._avgBytesThreshold && avgCookieSize < this._maxBytesThreshold)
1054 bigAvgCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(domain) + ": " + Number.bytesToString(avgCookieSize));
1056 result.addChild(String.sprintf("The average cookie size for all requests on this page is %s", Number.bytesToString(avgAllCookiesSize)));
1059 if (hugeCookieDomains.length) {
1060 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);
1061 entry.addURLs(hugeCookieDomains);
1062 result.violationCount += hugeCookieDomains.length;
1065 if (bigAvgCookieDomains.length) {
1066 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);
1067 entry.addURLs(bigAvgCookieDomains);
1068 result.violationCount += bigAvgCookieDomains.length;
1073 WebInspector.AuditRules.CookieSizeRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype;
1077 * @extends {WebInspector.AuditRules.CookieRuleBase}
1079 WebInspector.AuditRules.StaticCookielessRule = function(minResources)
1081 WebInspector.AuditRules.CookieRuleBase.call(this, "http-staticcookieless", "Serve static content from a cookieless domain");
1082 this._minResources = minResources;
1085 WebInspector.AuditRules.StaticCookielessRule.prototype = {
1086 processCookies: function(allCookies, resources, result)
1088 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources,
1089 [WebInspector.Resource.Type.Stylesheet,
1090 WebInspector.Resource.Type.Image],
1092 var totalStaticResources = 0;
1093 for (var domain in domainToResourcesMap)
1094 totalStaticResources += domainToResourcesMap[domain].length;
1095 if (totalStaticResources < this._minResources)
1097 var matchingResourceData = {};
1098 this.mapResourceCookies(domainToResourcesMap, allCookies, this._collectorCallback.bind(this, matchingResourceData));
1101 var cookieBytes = 0;
1102 for (var url in matchingResourceData) {
1104 cookieBytes += matchingResourceData[url]
1106 if (badUrls.length < this._minResources)
1109 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);
1110 entry.addURLs(badUrls);
1111 result.violationCount = badUrls.length;
1114 _collectorCallback: function(matchingResourceData, resource, cookie)
1116 matchingResourceData[resource.url] = (matchingResourceData[resource.url] || 0) + cookie.size;
1120 WebInspector.AuditRules.StaticCookielessRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype;