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 request is cacheable
46 * @param {!Array.<!WebInspector.NetworkRequest>} requests
47 * @param {Array.<!WebInspector.resourceTypes>} types
48 * @param {boolean} needFullResources
49 * @return {(Object.<string, !Array.<!WebInspector.NetworkRequest>>|Object.<string, !Array.<string>>)}
51 WebInspector.AuditRules.getDomainToResourcesMap = function(requests, types, needFullResources)
53 var domainToResourcesMap = {};
54 for (var i = 0, size = requests.length; i < size; ++i) {
55 var request = requests[i];
56 if (types && types.indexOf(request.type) === -1)
58 var parsedURL = request.url.asParsedURL();
61 var domain = parsedURL.host;
62 var domainResources = domainToResourcesMap[domain];
63 if (domainResources === undefined) {
65 domainToResourcesMap[domain] = domainResources;
67 domainResources.push(needFullResources ? request : request.url);
69 return domainToResourcesMap;
74 * @extends {WebInspector.AuditRule}
76 WebInspector.AuditRules.GzipRule = function()
78 WebInspector.AuditRule.call(this, "network-gzip", "Enable gzip compression");
81 WebInspector.AuditRules.GzipRule.prototype = {
83 * @param {!Array.<!WebInspector.NetworkRequest>} requests
84 * @param {!WebInspector.AuditRuleResult} result
85 * @param {function(WebInspector.AuditRuleResult)} callback
86 * @param {!WebInspector.Progress} progress
88 doRun: function(requests, result, callback, progress)
91 var compressedSize = 0;
92 var candidateSize = 0;
93 var summary = result.addChild("", true);
94 for (var i = 0, length = requests.length; i < length; ++i) {
95 var request = requests[i];
96 if (request.statusCode === 304)
97 continue; // Do not test 304 Not Modified requests as their contents are always empty.
98 if (this._shouldCompress(request)) {
99 var size = request.resourceSize;
100 candidateSize += size;
101 if (this._isCompressed(request)) {
102 compressedSize += size;
105 var savings = 2 * size / 3;
106 totalSavings += savings;
107 summary.addFormatted("%r could save ~%s", request.url, Number.bytesToString(savings));
108 result.violationCount++;
112 return callback(null);
113 summary.value = String.sprintf("Compressing the following resources with gzip could reduce their transfer size by about two thirds (~%s):", Number.bytesToString(totalSavings));
117 _isCompressed: function(request)
119 var encodingHeader = request.responseHeaderValue("Content-Encoding");
123 return /\b(?:gzip|deflate)\b/.test(encodingHeader);
126 _shouldCompress: function(request)
128 return request.type.isTextType() && request.parsedURL.host && request.resourceSize !== undefined && request.resourceSize > 150;
131 __proto__: WebInspector.AuditRule.prototype
136 * @extends {WebInspector.AuditRule}
138 WebInspector.AuditRules.CombineExternalResourcesRule = function(id, name, type, resourceTypeName, allowedPerDomain)
140 WebInspector.AuditRule.call(this, id, name);
142 this._resourceTypeName = resourceTypeName;
143 this._allowedPerDomain = allowedPerDomain;
146 WebInspector.AuditRules.CombineExternalResourcesRule.prototype = {
148 * @param {!Array.<!WebInspector.NetworkRequest>} requests
149 * @param {!WebInspector.AuditRuleResult} result
150 * @param {function(WebInspector.AuditRuleResult)} callback
151 * @param {!WebInspector.Progress} progress
153 doRun: function(requests, result, callback, progress)
155 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests, [this._type], false);
156 var penalizedResourceCount = 0;
157 // TODO: refactor according to the chosen i18n approach
158 var summary = result.addChild("", true);
159 for (var domain in domainToResourcesMap) {
160 var domainResources = domainToResourcesMap[domain];
161 var extraResourceCount = domainResources.length - this._allowedPerDomain;
162 if (extraResourceCount <= 0)
164 penalizedResourceCount += extraResourceCount - 1;
165 summary.addChild(String.sprintf("%d %s resources served from %s.", domainResources.length, this._resourceTypeName, WebInspector.AuditRuleResult.resourceDomain(domain)));
166 result.violationCount += domainResources.length;
168 if (!penalizedResourceCount)
169 return callback(null);
171 summary.value = "There are multiple resources served from same domain. Consider combining them into as few files as possible.";
175 __proto__: WebInspector.AuditRule.prototype
180 * @extends {WebInspector.AuditRules.CombineExternalResourcesRule}
182 WebInspector.AuditRules.CombineJsResourcesRule = function(allowedPerDomain) {
183 WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externaljs", "Combine external JavaScript", WebInspector.resourceTypes.Script, "JavaScript", allowedPerDomain);
186 WebInspector.AuditRules.CombineJsResourcesRule.prototype = {
187 __proto__: WebInspector.AuditRules.CombineExternalResourcesRule.prototype
192 * @extends {WebInspector.AuditRules.CombineExternalResourcesRule}
194 WebInspector.AuditRules.CombineCssResourcesRule = function(allowedPerDomain) {
195 WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externalcss", "Combine external CSS", WebInspector.resourceTypes.Stylesheet, "CSS", allowedPerDomain);
198 WebInspector.AuditRules.CombineCssResourcesRule.prototype = {
199 __proto__: WebInspector.AuditRules.CombineExternalResourcesRule.prototype
204 * @extends {WebInspector.AuditRule}
206 WebInspector.AuditRules.MinimizeDnsLookupsRule = function(hostCountThreshold) {
207 WebInspector.AuditRule.call(this, "network-minimizelookups", "Minimize DNS lookups");
208 this._hostCountThreshold = hostCountThreshold;
211 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype = {
213 * @param {!Array.<!WebInspector.NetworkRequest>} requests
214 * @param {!WebInspector.AuditRuleResult} result
215 * @param {function(WebInspector.AuditRuleResult)} callback
216 * @param {!WebInspector.Progress} progress
218 doRun: function(requests, result, callback, progress)
220 var summary = result.addChild("");
221 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests, null, false);
222 for (var domain in domainToResourcesMap) {
223 if (domainToResourcesMap[domain].length > 1)
225 var parsedURL = domain.asParsedURL();
228 if (!parsedURL.host.search(WebInspector.AuditRules.IPAddressRegexp))
229 continue; // an IP address
230 summary.addSnippet(domain);
231 result.violationCount++;
233 if (!summary.children || summary.children.length <= this._hostCountThreshold)
234 return callback(null);
236 summary.value = "The following domains only serve one resource each. If possible, avoid the extra DNS lookups by serving these resources from existing domains.";
240 __proto__: WebInspector.AuditRule.prototype
245 * @extends {WebInspector.AuditRule}
247 WebInspector.AuditRules.ParallelizeDownloadRule = function(optimalHostnameCount, minRequestThreshold, minBalanceThreshold)
249 WebInspector.AuditRule.call(this, "network-parallelizehosts", "Parallelize downloads across hostnames");
250 this._optimalHostnameCount = optimalHostnameCount;
251 this._minRequestThreshold = minRequestThreshold;
252 this._minBalanceThreshold = minBalanceThreshold;
255 WebInspector.AuditRules.ParallelizeDownloadRule.prototype = {
257 * @param {!Array.<!WebInspector.NetworkRequest>} requests
258 * @param {!WebInspector.AuditRuleResult} result
259 * @param {function(WebInspector.AuditRuleResult)} callback
260 * @param {!WebInspector.Progress} progress
262 doRun: function(requests, result, callback, progress)
264 function hostSorter(a, b)
266 var aCount = domainToResourcesMap[a].length;
267 var bCount = domainToResourcesMap[b].length;
268 return (aCount < bCount) ? 1 : (aCount == bCount) ? 0 : -1;
271 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(
273 [WebInspector.resourceTypes.Stylesheet, WebInspector.resourceTypes.Image],
277 for (var url in domainToResourcesMap)
281 return callback(null); // no hosts (local file or something)
283 hosts.sort(hostSorter);
285 var optimalHostnameCount = this._optimalHostnameCount;
286 if (hosts.length > optimalHostnameCount)
287 hosts.splice(optimalHostnameCount);
289 var busiestHostResourceCount = domainToResourcesMap[hosts[0]].length;
290 var requestCountAboveThreshold = busiestHostResourceCount - this._minRequestThreshold;
291 if (requestCountAboveThreshold <= 0)
292 return callback(null);
294 var avgResourcesPerHost = 0;
295 for (var i = 0, size = hosts.length; i < size; ++i)
296 avgResourcesPerHost += domainToResourcesMap[hosts[i]].length;
298 // Assume optimal parallelization.
299 avgResourcesPerHost /= optimalHostnameCount;
300 avgResourcesPerHost = Math.max(avgResourcesPerHost, 1);
302 var pctAboveAvg = (requestCountAboveThreshold / avgResourcesPerHost) - 1.0;
303 var minBalanceThreshold = this._minBalanceThreshold;
304 if (pctAboveAvg < minBalanceThreshold)
305 return callback(null);
307 var requestsOnBusiestHost = domainToResourcesMap[hosts[0]];
308 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);
309 for (var i = 0; i < requestsOnBusiestHost.length; ++i)
310 entry.addURL(requestsOnBusiestHost[i].url);
312 result.violationCount = requestsOnBusiestHost.length;
316 __proto__: WebInspector.AuditRule.prototype
320 * The reported CSS rule size is incorrect (parsed != original in WebKit),
321 * so use percentages instead, which gives a better approximation.
323 * @extends {WebInspector.AuditRule}
325 WebInspector.AuditRules.UnusedCssRule = function()
327 WebInspector.AuditRule.call(this, "page-unusedcss", "Remove unused CSS rules");
330 WebInspector.AuditRules.UnusedCssRule.prototype = {
332 * @param {!Array.<!WebInspector.NetworkRequest>} requests
333 * @param {!WebInspector.AuditRuleResult} result
334 * @param {function(WebInspector.AuditRuleResult)} callback
335 * @param {!WebInspector.Progress} progress
337 doRun: function(requests, result, callback, progress)
341 function evalCallback(styleSheets) {
342 if (progress.isCanceled())
345 if (!styleSheets.length)
346 return callback(null);
349 var testedSelectors = {};
350 for (var i = 0; i < styleSheets.length; ++i) {
351 var styleSheet = styleSheets[i];
352 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
353 var selectorText = styleSheet.rules[curRule].selectorText;
354 if (testedSelectors[selectorText])
356 selectors.push(selectorText);
357 testedSelectors[selectorText] = 1;
361 function selectorsCallback(callback, styleSheets, testedSelectors, foundSelectors)
363 if (progress.isCanceled())
366 var inlineBlockOrdinal = 0;
367 var totalStylesheetSize = 0;
368 var totalUnusedStylesheetSize = 0;
371 for (var i = 0; i < styleSheets.length; ++i) {
372 var styleSheet = styleSheets[i];
373 var unusedRules = [];
374 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
375 var rule = styleSheet.rules[curRule];
376 if (!testedSelectors[rule.selectorText] || foundSelectors[rule.selectorText])
378 unusedRules.push(rule.selectorText);
380 totalStylesheetSize += styleSheet.rules.length;
381 totalUnusedStylesheetSize += unusedRules.length;
383 if (!unusedRules.length)
386 var resource = WebInspector.resourceForURL(styleSheet.sourceURL);
387 var isInlineBlock = resource && resource.request && resource.request.type == WebInspector.resourceTypes.Document;
388 var url = !isInlineBlock ? WebInspector.AuditRuleResult.linkifyDisplayName(styleSheet.sourceURL) : String.sprintf("Inline block #%d", ++inlineBlockOrdinal);
389 var pctUnused = Math.round(100 * unusedRules.length / styleSheet.rules.length);
391 summary = result.addChild("", true);
392 var entry = summary.addFormatted("%s: %d% is not used by the current page.", url, pctUnused);
394 for (var j = 0; j < unusedRules.length; ++j)
395 entry.addSnippet(unusedRules[j]);
397 result.violationCount += unusedRules.length;
400 if (!totalUnusedStylesheetSize)
401 return callback(null);
403 var totalUnusedPercent = Math.round(100 * totalUnusedStylesheetSize / totalStylesheetSize);
404 summary.value = String.sprintf("%s rules (%d%) of CSS not used by the current page.", totalUnusedStylesheetSize, totalUnusedPercent);
409 var foundSelectors = {};
410 function queryCallback(boundSelectorsCallback, selector, styleSheets, testedSelectors, nodeId)
413 foundSelectors[selector] = true;
414 if (boundSelectorsCallback)
415 boundSelectorsCallback(foundSelectors);
418 function documentLoaded(selectors, document) {
419 var pseudoSelectorRegexp = /::?(?:[\w-]+)(?:\(.*?\))?/g;
420 for (var i = 0; i < selectors.length; ++i) {
421 if (progress.isCanceled())
423 var effectiveSelector = selectors[i].replace(pseudoSelectorRegexp, "");
424 WebInspector.domAgent.querySelector(document.id, effectiveSelector, queryCallback.bind(null, i === selectors.length - 1 ? selectorsCallback.bind(null, callback, styleSheets, testedSelectors) : null, selectors[i], styleSheets, testedSelectors));
428 WebInspector.domAgent.requestDocument(documentLoaded.bind(null, selectors));
431 function styleSheetCallback(styleSheets, sourceURL, continuation, styleSheet)
433 if (progress.isCanceled())
437 styleSheet.sourceURL = sourceURL;
438 styleSheets.push(styleSheet);
441 continuation(styleSheets);
444 function allStylesCallback(error, styleSheetInfos)
446 if (progress.isCanceled())
449 if (error || !styleSheetInfos || !styleSheetInfos.length)
450 return evalCallback([]);
451 var styleSheets = [];
452 for (var i = 0; i < styleSheetInfos.length; ++i) {
453 var info = styleSheetInfos[i];
454 WebInspector.CSSStyleSheet.createForId(info.styleSheetId, styleSheetCallback.bind(null, styleSheets, info.sourceURL, i == styleSheetInfos.length - 1 ? evalCallback : null));
458 CSSAgent.getAllStyleSheets(allStylesCallback);
461 __proto__: WebInspector.AuditRule.prototype
466 * @extends {WebInspector.AuditRule}
468 WebInspector.AuditRules.CacheControlRule = function(id, name)
470 WebInspector.AuditRule.call(this, id, name);
473 WebInspector.AuditRules.CacheControlRule.MillisPerMonth = 1000 * 60 * 60 * 24 * 30;
475 WebInspector.AuditRules.CacheControlRule.prototype = {
477 * @param {!Array.<!WebInspector.NetworkRequest>} requests
478 * @param {!WebInspector.AuditRuleResult} result
479 * @param {function(WebInspector.AuditRuleResult)} callback
480 * @param {!WebInspector.Progress} progress
482 doRun: function(requests, result, callback, progress)
484 var cacheableAndNonCacheableResources = this._cacheableAndNonCacheableResources(requests);
485 if (cacheableAndNonCacheableResources[0].length)
486 this.runChecks(cacheableAndNonCacheableResources[0], result);
487 this.handleNonCacheableResources(cacheableAndNonCacheableResources[1], result);
492 handleNonCacheableResources: function(requests, result)
496 _cacheableAndNonCacheableResources: function(requests)
498 var processedResources = [[], []];
499 for (var i = 0; i < requests.length; ++i) {
500 var request = requests[i];
501 if (!this.isCacheableResource(request))
503 if (this._isExplicitlyNonCacheable(request))
504 processedResources[1].push(request);
506 processedResources[0].push(request);
508 return processedResources;
511 execCheck: function(messageText, requestCheckFunction, requests, result)
513 var requestCount = requests.length;
515 for (var i = 0; i < requestCount; ++i) {
516 if (requestCheckFunction.call(this, requests[i]))
517 urls.push(requests[i].url);
520 var entry = result.addChild(messageText, true);
522 result.violationCount += urls.length;
526 freshnessLifetimeGreaterThan: function(request, timeMs)
528 var dateHeader = this.responseHeader(request, "Date");
532 var dateHeaderMs = Date.parse(dateHeader);
533 if (isNaN(dateHeaderMs))
536 var freshnessLifetimeMs;
537 var maxAgeMatch = this.responseHeaderMatch(request, "Cache-Control", "max-age=(\\d+)");
540 freshnessLifetimeMs = (maxAgeMatch[1]) ? 1000 * maxAgeMatch[1] : 0;
542 var expiresHeader = this.responseHeader(request, "Expires");
544 var expDate = Date.parse(expiresHeader);
546 freshnessLifetimeMs = expDate - dateHeaderMs;
550 return (isNaN(freshnessLifetimeMs)) ? false : freshnessLifetimeMs > timeMs;
553 responseHeader: function(request, header)
555 return request.responseHeaderValue(header);
558 hasResponseHeader: function(request, header)
560 return request.responseHeaderValue(header) !== undefined;
563 isCompressible: function(request)
565 return request.type.isTextType();
568 isPubliclyCacheable: function(request)
570 if (this._isExplicitlyNonCacheable(request))
573 if (this.responseHeaderMatch(request, "Cache-Control", "public"))
576 return request.url.indexOf("?") == -1 && !this.responseHeaderMatch(request, "Cache-Control", "private");
579 responseHeaderMatch: function(request, header, regexp)
581 return request.responseHeaderValue(header)
582 ? request.responseHeaderValue(header).match(new RegExp(regexp, "im"))
586 hasExplicitExpiration: function(request)
588 return this.hasResponseHeader(request, "Date") &&
589 (this.hasResponseHeader(request, "Expires") || this.responseHeaderMatch(request, "Cache-Control", "max-age"));
592 _isExplicitlyNonCacheable: function(request)
594 var hasExplicitExp = this.hasExplicitExpiration(request);
595 return this.responseHeaderMatch(request, "Cache-Control", "(no-cache|no-store|must-revalidate)") ||
596 this.responseHeaderMatch(request, "Pragma", "no-cache") ||
597 (hasExplicitExp && !this.freshnessLifetimeGreaterThan(request, 0)) ||
598 (!hasExplicitExp && request.url && request.url.indexOf("?") >= 0) ||
599 (!hasExplicitExp && !this.isCacheableResource(request));
602 isCacheableResource: function(request)
604 return request.statusCode !== undefined && WebInspector.AuditRules.CacheableResponseCodes[request.statusCode];
607 __proto__: WebInspector.AuditRule.prototype
612 * @extends {WebInspector.AuditRules.CacheControlRule}
614 WebInspector.AuditRules.BrowserCacheControlRule = function()
616 WebInspector.AuditRules.CacheControlRule.call(this, "http-browsercache", "Leverage browser caching");
619 WebInspector.AuditRules.BrowserCacheControlRule.prototype = {
620 handleNonCacheableResources: function(requests, result)
622 if (requests.length) {
623 var entry = result.addChild("The following resources are explicitly non-cacheable. Consider making them cacheable if possible:", true);
624 result.violationCount += requests.length;
625 for (var i = 0; i < requests.length; ++i)
626 entry.addURL(requests[i].url);
630 runChecks: function(requests, result, callback)
632 this.execCheck("The following resources are missing a cache expiration. Resources that do not specify an expiration may not be cached by browsers:",
633 this._missingExpirationCheck, requests, result);
634 this.execCheck("The following resources specify a \"Vary\" header that disables caching in most versions of Internet Explorer:",
635 this._varyCheck, requests, result);
636 this.execCheck("The following cacheable resources have a short freshness lifetime:",
637 this._oneMonthExpirationCheck, requests, result);
639 // Unable to implement the favicon check due to the WebKit limitations.
640 this.execCheck("To further improve cache hit rate, specify an expiration one year in the future for the following cacheable resources:",
641 this._oneYearExpirationCheck, requests, result);
644 _missingExpirationCheck: function(request)
646 return this.isCacheableResource(request) && !this.hasResponseHeader(request, "Set-Cookie") && !this.hasExplicitExpiration(request);
649 _varyCheck: function(request)
651 var varyHeader = this.responseHeader(request, "Vary");
653 varyHeader = varyHeader.replace(/User-Agent/gi, "");
654 varyHeader = varyHeader.replace(/Accept-Encoding/gi, "");
655 varyHeader = varyHeader.replace(/[, ]*/g, "");
657 return varyHeader && varyHeader.length && this.isCacheableResource(request) && this.freshnessLifetimeGreaterThan(request, 0);
660 _oneMonthExpirationCheck: function(request)
662 return this.isCacheableResource(request) &&
663 !this.hasResponseHeader(request, "Set-Cookie") &&
664 !this.freshnessLifetimeGreaterThan(request, WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
665 this.freshnessLifetimeGreaterThan(request, 0);
668 _oneYearExpirationCheck: function(request)
670 return this.isCacheableResource(request) &&
671 !this.hasResponseHeader(request, "Set-Cookie") &&
672 !this.freshnessLifetimeGreaterThan(request, 11 * WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
673 this.freshnessLifetimeGreaterThan(request, WebInspector.AuditRules.CacheControlRule.MillisPerMonth);
676 __proto__: WebInspector.AuditRules.CacheControlRule.prototype
681 * @extends {WebInspector.AuditRules.CacheControlRule}
683 WebInspector.AuditRules.ProxyCacheControlRule = function() {
684 WebInspector.AuditRules.CacheControlRule.call(this, "http-proxycache", "Leverage proxy caching");
687 WebInspector.AuditRules.ProxyCacheControlRule.prototype = {
688 runChecks: function(requests, result, callback)
690 this.execCheck("Resources with a \"?\" in the URL are not cached by most proxy caching servers:",
691 this._questionMarkCheck, requests, result);
692 this.execCheck("Consider adding a \"Cache-Control: public\" header to the following resources:",
693 this._publicCachingCheck, requests, result);
694 this.execCheck("The following publicly cacheable resources contain a Set-Cookie header. This security vulnerability can cause cookies to be shared by multiple users.",
695 this._setCookieCacheableCheck, requests, result);
698 _questionMarkCheck: function(request)
700 return request.url.indexOf("?") >= 0 && !this.hasResponseHeader(request, "Set-Cookie") && this.isPubliclyCacheable(request);
703 _publicCachingCheck: function(request)
705 return this.isCacheableResource(request) &&
706 !this.isCompressible(request) &&
707 !this.responseHeaderMatch(request, "Cache-Control", "public") &&
708 !this.hasResponseHeader(request, "Set-Cookie");
711 _setCookieCacheableCheck: function(request)
713 return this.hasResponseHeader(request, "Set-Cookie") && this.isPubliclyCacheable(request);
716 __proto__: WebInspector.AuditRules.CacheControlRule.prototype
721 * @extends {WebInspector.AuditRule}
723 WebInspector.AuditRules.ImageDimensionsRule = function()
725 WebInspector.AuditRule.call(this, "page-imagedims", "Specify image dimensions");
728 WebInspector.AuditRules.ImageDimensionsRule.prototype = {
730 * @param {!Array.<!WebInspector.NetworkRequest>} requests
731 * @param {!WebInspector.AuditRuleResult} result
732 * @param {function(WebInspector.AuditRuleResult)} callback
733 * @param {!WebInspector.Progress} progress
735 doRun: function(requests, result, callback, progress)
737 var urlToNoDimensionCount = {};
739 function doneCallback()
741 for (var url in urlToNoDimensionCount) {
742 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);
744 if (urlToNoDimensionCount[url] > 1)
745 format += " (%d uses)";
746 entry.addFormatted(format, url, urlToNoDimensionCount[url]);
747 result.violationCount++;
749 callback(entry ? result : null);
752 function imageStylesReady(imageId, styles, isLastStyle, computedStyle)
754 if (progress.isCanceled())
757 const node = WebInspector.domAgent.nodeForId(imageId);
758 var src = node.getAttribute("src");
759 if (!src.asParsedURL()) {
760 for (var frameOwnerCandidate = node; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) {
761 if (frameOwnerCandidate.baseURL) {
762 var completeSrc = WebInspector.ParsedURL.completeURL(frameOwnerCandidate.baseURL, src);
770 if (computedStyle.getPropertyValue("position") === "absolute") {
776 if (styles.attributesStyle) {
777 var widthFound = !!styles.attributesStyle.getLiveProperty("width");
778 var heightFound = !!styles.attributesStyle.getLiveProperty("height");
781 var inlineStyle = styles.inlineStyle;
783 if (inlineStyle.getPropertyValue("width") !== "")
785 if (inlineStyle.getPropertyValue("height") !== "")
789 for (var i = styles.matchedCSSRules.length - 1; i >= 0 && !(widthFound && heightFound); --i) {
790 var style = styles.matchedCSSRules[i].style;
791 if (style.getPropertyValue("width") !== "")
793 if (style.getPropertyValue("height") !== "")
797 if (!widthFound || !heightFound) {
798 if (src in urlToNoDimensionCount)
799 ++urlToNoDimensionCount[src];
801 urlToNoDimensionCount[src] = 1;
808 function getStyles(nodeIds)
810 if (progress.isCanceled())
812 var targetResult = {};
814 function inlineCallback(inlineStyle, attributesStyle)
816 targetResult.inlineStyle = inlineStyle;
817 targetResult.attributesStyle = attributesStyle;
820 function matchedCallback(result)
823 targetResult.matchedCSSRules = result.matchedCSSRules;
826 if (!nodeIds || !nodeIds.length)
829 for (var i = 0; nodeIds && i < nodeIds.length; ++i) {
830 WebInspector.cssModel.getMatchedStylesAsync(nodeIds[i], false, false, matchedCallback);
831 WebInspector.cssModel.getInlineStylesAsync(nodeIds[i], inlineCallback);
832 WebInspector.cssModel.getComputedStyleAsync(nodeIds[i], imageStylesReady.bind(null, nodeIds[i], targetResult, i === nodeIds.length - 1));
836 function onDocumentAvailable(root)
838 if (progress.isCanceled())
840 WebInspector.domAgent.querySelectorAll(root.id, "img[src]", getStyles);
843 if (progress.isCanceled())
845 WebInspector.domAgent.requestDocument(onDocumentAvailable);
848 __proto__: WebInspector.AuditRule.prototype
853 * @extends {WebInspector.AuditRule}
855 WebInspector.AuditRules.CssInHeadRule = function()
857 WebInspector.AuditRule.call(this, "page-cssinhead", "Put CSS in the document head");
860 WebInspector.AuditRules.CssInHeadRule.prototype = {
862 * @param {!Array.<!WebInspector.NetworkRequest>} requests
863 * @param {!WebInspector.AuditRuleResult} result
864 * @param {function(WebInspector.AuditRuleResult)} callback
865 * @param {!WebInspector.Progress} progress
867 doRun: function(requests, result, callback, progress)
869 function evalCallback(evalResult)
871 if (progress.isCanceled())
875 return callback(null);
877 var summary = result.addChild("");
879 var outputMessages = [];
880 for (var url in evalResult) {
881 var urlViolations = evalResult[url];
882 if (urlViolations[0]) {
883 result.addFormatted("%s style block(s) in the %r body should be moved to the document head.", urlViolations[0], url);
884 result.violationCount += urlViolations[0];
886 for (var i = 0; i < urlViolations[1].length; ++i)
887 result.addFormatted("Link node %r should be moved to the document head in %r", urlViolations[1][i], url);
888 result.violationCount += urlViolations[1].length;
890 summary.value = String.sprintf("CSS in the document body adversely impacts rendering performance.");
894 function externalStylesheetsReceived(root, inlineStyleNodeIds, nodeIds)
896 if (progress.isCanceled())
901 var externalStylesheetNodeIds = nodeIds;
903 if (inlineStyleNodeIds.length || externalStylesheetNodeIds.length) {
904 var urlToViolationsArray = {};
905 var externalStylesheetHrefs = [];
906 for (var j = 0; j < externalStylesheetNodeIds.length; ++j) {
907 var linkNode = WebInspector.domAgent.nodeForId(externalStylesheetNodeIds[j]);
908 var completeHref = WebInspector.ParsedURL.completeURL(linkNode.ownerDocument.baseURL, linkNode.getAttribute("href"));
909 externalStylesheetHrefs.push(completeHref || "<empty>");
911 urlToViolationsArray[root.documentURL] = [inlineStyleNodeIds.length, externalStylesheetHrefs];
912 result = urlToViolationsArray;
914 evalCallback(result);
917 function inlineStylesReceived(root, nodeIds)
919 if (progress.isCanceled())
924 WebInspector.domAgent.querySelectorAll(root.id, "body link[rel~='stylesheet'][href]", externalStylesheetsReceived.bind(null, root, nodeIds));
927 function onDocumentAvailable(root)
929 if (progress.isCanceled())
932 WebInspector.domAgent.querySelectorAll(root.id, "body style", inlineStylesReceived.bind(null, root));
935 WebInspector.domAgent.requestDocument(onDocumentAvailable);
938 __proto__: WebInspector.AuditRule.prototype
943 * @extends {WebInspector.AuditRule}
945 WebInspector.AuditRules.StylesScriptsOrderRule = function()
947 WebInspector.AuditRule.call(this, "page-stylescriptorder", "Optimize the order of styles and scripts");
950 WebInspector.AuditRules.StylesScriptsOrderRule.prototype = {
952 * @param {!Array.<!WebInspector.NetworkRequest>} requests
953 * @param {!WebInspector.AuditRuleResult} result
954 * @param {function(WebInspector.AuditRuleResult)} callback
955 * @param {!WebInspector.Progress} progress
957 doRun: function(requests, result, callback, progress)
959 function evalCallback(resultValue)
961 if (progress.isCanceled())
965 return callback(null);
967 var lateCssUrls = resultValue[0];
968 var cssBeforeInlineCount = resultValue[1];
970 if (lateCssUrls.length) {
971 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);
972 entry.addURLs(lateCssUrls);
973 result.violationCount += lateCssUrls.length;
976 if (cssBeforeInlineCount) {
977 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"));
978 result.violationCount += cssBeforeInlineCount;
983 function cssBeforeInlineReceived(lateStyleIds, nodeIds)
985 if (progress.isCanceled())
991 var cssBeforeInlineCount = nodeIds.length;
993 if (lateStyleIds.length || cssBeforeInlineCount) {
994 var lateStyleUrls = [];
995 for (var i = 0; i < lateStyleIds.length; ++i) {
996 var lateStyleNode = WebInspector.domAgent.nodeForId(lateStyleIds[i]);
997 var completeHref = WebInspector.ParsedURL.completeURL(lateStyleNode.ownerDocument.baseURL, lateStyleNode.getAttribute("href"));
998 lateStyleUrls.push(completeHref || "<empty>");
1000 result = [ lateStyleUrls, cssBeforeInlineCount ];
1003 evalCallback(result);
1006 function lateStylesReceived(root, nodeIds)
1008 if (progress.isCanceled())
1014 WebInspector.domAgent.querySelectorAll(root.id, "head link[rel~='stylesheet'][href] ~ script:not([src])", cssBeforeInlineReceived.bind(null, nodeIds));
1017 function onDocumentAvailable(root)
1019 if (progress.isCanceled())
1022 WebInspector.domAgent.querySelectorAll(root.id, "head script[src] ~ link[rel~='stylesheet'][href]", lateStylesReceived.bind(null, root));
1025 WebInspector.domAgent.requestDocument(onDocumentAvailable);
1028 __proto__: WebInspector.AuditRule.prototype
1033 * @extends {WebInspector.AuditRule}
1035 WebInspector.AuditRules.CSSRuleBase = function(id, name)
1037 WebInspector.AuditRule.call(this, id, name);
1040 WebInspector.AuditRules.CSSRuleBase.prototype = {
1042 * @param {!Array.<!WebInspector.NetworkRequest>} requests
1043 * @param {!WebInspector.AuditRuleResult} result
1044 * @param {function(WebInspector.AuditRuleResult)} callback
1045 * @param {!WebInspector.Progress} progress
1047 doRun: function(requests, result, callback, progress)
1049 CSSAgent.getAllStyleSheets(sheetsCallback.bind(this));
1051 function sheetsCallback(error, headers)
1054 return callback(null);
1056 if (!headers.length)
1057 return callback(null);
1058 for (var i = 0; i < headers.length; ++i) {
1059 var header = headers[i];
1060 if (header.disabled)
1061 continue; // Do not check disabled stylesheets.
1063 this._visitStyleSheet(header.styleSheetId, i === headers.length - 1 ? finishedCallback : null, result, progress);
1067 function finishedCallback()
1073 _visitStyleSheet: function(styleSheetId, callback, result, progress)
1075 WebInspector.CSSStyleSheet.createForId(styleSheetId, sheetCallback.bind(this));
1077 function sheetCallback(styleSheet)
1079 if (progress.isCanceled())
1088 this.visitStyleSheet(styleSheet, result);
1090 for (var i = 0; i < styleSheet.rules.length; ++i)
1091 this._visitRule(styleSheet, styleSheet.rules[i], result);
1093 this.didVisitStyleSheet(styleSheet, result);
1100 _visitRule: function(styleSheet, rule, result)
1102 this.visitRule(styleSheet, rule, result);
1103 var allProperties = rule.style.allProperties;
1104 for (var i = 0; i < allProperties.length; ++i)
1105 this.visitProperty(styleSheet, allProperties[i], result);
1106 this.didVisitRule(styleSheet, rule, result);
1109 visitStyleSheet: function(styleSheet, result)
1111 // Subclasses can implement.
1114 didVisitStyleSheet: function(styleSheet, result)
1116 // Subclasses can implement.
1119 visitRule: function(styleSheet, rule, result)
1121 // Subclasses can implement.
1124 didVisitRule: function(styleSheet, rule, result)
1126 // Subclasses can implement.
1129 visitProperty: function(styleSheet, property, result)
1131 // Subclasses can implement.
1134 __proto__: WebInspector.AuditRule.prototype
1139 * @extends {WebInspector.AuditRules.CSSRuleBase}
1141 WebInspector.AuditRules.VendorPrefixedCSSProperties = function()
1143 WebInspector.AuditRules.CSSRuleBase.call(this, "page-vendorprefixedcss", "Use normal CSS property names instead of vendor-prefixed ones");
1144 this._webkitPrefix = "-webkit-";
1147 WebInspector.AuditRules.VendorPrefixedCSSProperties.supportedProperties = [
1148 "background-clip", "background-origin", "background-size",
1149 "border-radius", "border-bottom-left-radius", "border-bottom-right-radius", "border-top-left-radius", "border-top-right-radius",
1150 "box-shadow", "box-sizing", "opacity", "text-shadow"
1153 WebInspector.AuditRules.VendorPrefixedCSSProperties.prototype = {
1154 didVisitStyleSheet: function(styleSheet)
1156 delete this._styleSheetResult;
1159 visitRule: function(rule)
1161 this._mentionedProperties = {};
1164 didVisitRule: function()
1166 delete this._ruleResult;
1167 delete this._mentionedProperties;
1170 visitProperty: function(styleSheet, property, result)
1172 if (!property.name.startsWith(this._webkitPrefix))
1175 var normalPropertyName = property.name.substring(this._webkitPrefix.length).toLowerCase(); // Start just after the "-webkit-" prefix.
1176 if (WebInspector.AuditRules.VendorPrefixedCSSProperties.supportedProperties[normalPropertyName] && !this._mentionedProperties[normalPropertyName]) {
1177 var style = property.ownerStyle;
1178 var liveProperty = style.getLiveProperty(normalPropertyName);
1179 if (liveProperty && !liveProperty.styleBased)
1180 return; // WebCore can provide normal versions of prefixed properties automatically, so be careful to skip only normal source-based properties.
1182 var rule = style.parentRule;
1183 this._mentionedProperties[normalPropertyName] = true;
1184 if (!this._styleSheetResult)
1185 this._styleSheetResult = result.addChild(rule.sourceURL ? WebInspector.linkifyResourceAsNode(rule.sourceURL) : "<unknown>");
1186 if (!this._ruleResult) {
1187 var anchor = WebInspector.linkifyURLAsNode(rule.sourceURL, rule.selectorText);
1188 anchor.preferredPanel = "resources";
1189 anchor.lineNumber = rule.lineNumberInSource();
1190 this._ruleResult = this._styleSheetResult.addChild(anchor);
1192 ++result.violationCount;
1193 this._ruleResult.addSnippet(String.sprintf("\"" + this._webkitPrefix + "%s\" is used, but \"%s\" is supported.", normalPropertyName, normalPropertyName));
1197 __proto__: WebInspector.AuditRules.CSSRuleBase.prototype
1202 * @extends {WebInspector.AuditRule}
1204 WebInspector.AuditRules.CookieRuleBase = function(id, name)
1206 WebInspector.AuditRule.call(this, id, name);
1209 WebInspector.AuditRules.CookieRuleBase.prototype = {
1211 * @param {!Array.<!WebInspector.NetworkRequest>} requests
1212 * @param {!WebInspector.AuditRuleResult} result
1213 * @param {function(WebInspector.AuditRuleResult)} callback
1214 * @param {!WebInspector.Progress} progress
1216 doRun: function(requests, result, callback, progress)
1219 function resultCallback(receivedCookies) {
1220 if (progress.isCanceled())
1223 self.processCookies(receivedCookies, requests, result);
1227 WebInspector.Cookies.getCookiesAsync(resultCallback);
1230 mapResourceCookies: function(requestsByDomain, allCookies, callback)
1232 for (var i = 0; i < allCookies.length; ++i) {
1233 for (var requestDomain in requestsByDomain) {
1234 if (WebInspector.Cookies.cookieDomainMatchesResourceDomain(allCookies[i].domain(), requestDomain))
1235 this._callbackForResourceCookiePairs(requestsByDomain[requestDomain], allCookies[i], callback);
1240 _callbackForResourceCookiePairs: function(requests, cookie, callback)
1244 for (var i = 0; i < requests.length; ++i) {
1245 if (WebInspector.Cookies.cookieMatchesResourceURL(cookie, requests[i].url))
1246 callback(requests[i], cookie);
1250 __proto__: WebInspector.AuditRule.prototype
1255 * @extends {WebInspector.AuditRules.CookieRuleBase}
1257 WebInspector.AuditRules.CookieSizeRule = function(avgBytesThreshold)
1259 WebInspector.AuditRules.CookieRuleBase.call(this, "http-cookiesize", "Minimize cookie size");
1260 this._avgBytesThreshold = avgBytesThreshold;
1261 this._maxBytesThreshold = 1000;
1264 WebInspector.AuditRules.CookieSizeRule.prototype = {
1265 _average: function(cookieArray)
1268 for (var i = 0; i < cookieArray.length; ++i)
1269 total += cookieArray[i].size();
1270 return cookieArray.length ? Math.round(total / cookieArray.length) : 0;
1273 _max: function(cookieArray)
1276 for (var i = 0; i < cookieArray.length; ++i)
1277 result = Math.max(cookieArray[i].size(), result);
1281 processCookies: function(allCookies, requests, result)
1283 function maxSizeSorter(a, b)
1285 return b.maxCookieSize - a.maxCookieSize;
1288 function avgSizeSorter(a, b)
1290 return b.avgCookieSize - a.avgCookieSize;
1293 var cookiesPerResourceDomain = {};
1295 function collectorCallback(request, cookie)
1297 var cookies = cookiesPerResourceDomain[request.parsedURL.host];
1300 cookiesPerResourceDomain[request.parsedURL.host] = cookies;
1302 cookies.push(cookie);
1305 if (!allCookies.length)
1308 var sortedCookieSizes = [];
1310 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests,
1313 var matchingResourceData = {};
1314 this.mapResourceCookies(domainToResourcesMap, allCookies, collectorCallback.bind(this));
1316 for (var requestDomain in cookiesPerResourceDomain) {
1317 var cookies = cookiesPerResourceDomain[requestDomain];
1318 sortedCookieSizes.push({
1319 domain: requestDomain,
1320 avgCookieSize: this._average(cookies),
1321 maxCookieSize: this._max(cookies)
1324 var avgAllCookiesSize = this._average(allCookies);
1326 var hugeCookieDomains = [];
1327 sortedCookieSizes.sort(maxSizeSorter);
1329 for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
1330 var maxCookieSize = sortedCookieSizes[i].maxCookieSize;
1331 if (maxCookieSize > this._maxBytesThreshold)
1332 hugeCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(sortedCookieSizes[i].domain) + ": " + Number.bytesToString(maxCookieSize));
1335 var bigAvgCookieDomains = [];
1336 sortedCookieSizes.sort(avgSizeSorter);
1337 for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
1338 var domain = sortedCookieSizes[i].domain;
1339 var avgCookieSize = sortedCookieSizes[i].avgCookieSize;
1340 if (avgCookieSize > this._avgBytesThreshold && avgCookieSize < this._maxBytesThreshold)
1341 bigAvgCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(domain) + ": " + Number.bytesToString(avgCookieSize));
1343 result.addChild(String.sprintf("The average cookie size for all requests on this page is %s", Number.bytesToString(avgAllCookiesSize)));
1346 if (hugeCookieDomains.length) {
1347 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);
1348 entry.addURLs(hugeCookieDomains);
1349 result.violationCount += hugeCookieDomains.length;
1352 if (bigAvgCookieDomains.length) {
1353 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);
1354 entry.addURLs(bigAvgCookieDomains);
1355 result.violationCount += bigAvgCookieDomains.length;
1359 __proto__: WebInspector.AuditRules.CookieRuleBase.prototype
1364 * @extends {WebInspector.AuditRules.CookieRuleBase}
1366 WebInspector.AuditRules.StaticCookielessRule = function(minResources)
1368 WebInspector.AuditRules.CookieRuleBase.call(this, "http-staticcookieless", "Serve static content from a cookieless domain");
1369 this._minResources = minResources;
1372 WebInspector.AuditRules.StaticCookielessRule.prototype = {
1373 processCookies: function(allCookies, requests, result)
1375 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests,
1376 [WebInspector.resourceTypes.Stylesheet,
1377 WebInspector.resourceTypes.Image],
1379 var totalStaticResources = 0;
1380 for (var domain in domainToResourcesMap)
1381 totalStaticResources += domainToResourcesMap[domain].length;
1382 if (totalStaticResources < this._minResources)
1384 var matchingResourceData = {};
1385 this.mapResourceCookies(domainToResourcesMap, allCookies, this._collectorCallback.bind(this, matchingResourceData));
1388 var cookieBytes = 0;
1389 for (var url in matchingResourceData) {
1391 cookieBytes += matchingResourceData[url]
1393 if (badUrls.length < this._minResources)
1396 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);
1397 entry.addURLs(badUrls);
1398 result.violationCount = badUrls.length;
1401 _collectorCallback: function(matchingResourceData, request, cookie)
1403 matchingResourceData[request.url] = (matchingResourceData[request.url] || 0) + cookie.size();
1406 __proto__: WebInspector.AuditRules.CookieRuleBase.prototype