Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / audits / AuditRules.js
blobbf7067cce18e7290a3aedcffde3083034354b476
1 /*
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
6 * met:
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
13 * distribution.
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 =
35 200: true,
36 203: true,
37 206: true,
38 300: true,
39 301: true,
40 410: true,
42 304: true // Underlying request is cacheable
45 /**
46 * @param {!Array.<!WebInspector.NetworkRequest>} requests
47 * @param {?Array.<!WebInspector.resourceTypes>} types
48 * @param {boolean} needFullResources
49 * @return {!Object.<string, !Array.<!WebInspector.NetworkRequest|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.resourceType()) === -1)
57 continue;
58 var parsedURL = request.url.asParsedURL();
59 if (!parsedURL)
60 continue;
61 var domain = parsedURL.host;
62 var domainResources = domainToResourcesMap[domain];
63 if (domainResources === undefined) {
64 domainResources = [];
65 domainToResourcesMap[domain] = domainResources;
67 domainResources.push(needFullResources ? request : request.url);
69 return domainToResourcesMap;
72 /**
73 * @constructor
74 * @extends {WebInspector.AuditRule}
76 WebInspector.AuditRules.GzipRule = function()
78 WebInspector.AuditRule.call(this, "network-gzip", WebInspector.UIString("Enable gzip compression"));
81 WebInspector.AuditRules.GzipRule.prototype = {
82 /**
83 * @override
84 * @param {!WebInspector.Target} target
85 * @param {!Array.<!WebInspector.NetworkRequest>} requests
86 * @param {!WebInspector.AuditRuleResult} result
87 * @param {function(?WebInspector.AuditRuleResult)} callback
88 * @param {!WebInspector.Progress} progress
90 doRun: function(target, requests, result, callback, progress)
92 var totalSavings = 0;
93 var compressedSize = 0;
94 var candidateSize = 0;
95 var summary = result.addChild("", true);
96 for (var i = 0, length = requests.length; i < length; ++i) {
97 var request = requests[i];
98 if (request.cached() || request.statusCode === 304)
99 continue; // Do not test cached resources.
100 if (this._shouldCompress(request)) {
101 var size = request.resourceSize;
102 candidateSize += size;
103 if (this._isCompressed(request)) {
104 compressedSize += size;
105 continue;
107 var savings = 2 * size / 3;
108 totalSavings += savings;
109 summary.addFormatted("%r could save ~%s", request.url, Number.bytesToString(savings));
110 result.violationCount++;
113 if (!totalSavings) {
114 callback(null);
115 return;
117 summary.value = WebInspector.UIString("Compressing the following resources with gzip could reduce their transfer size by about two thirds (~%s):", Number.bytesToString(totalSavings));
118 callback(result);
122 * @param {!WebInspector.NetworkRequest} request
124 _isCompressed: function(request)
126 var encodingHeader = request.responseHeaderValue("Content-Encoding");
127 if (!encodingHeader)
128 return false;
130 return /\b(?:gzip|deflate)\b/.test(encodingHeader);
134 * @param {!WebInspector.NetworkRequest} request
136 _shouldCompress: function(request)
138 return request.resourceType().isTextType() && request.parsedURL.host && request.resourceSize !== undefined && request.resourceSize > 150;
141 __proto__: WebInspector.AuditRule.prototype
145 * @constructor
146 * @extends {WebInspector.AuditRule}
147 * @param {string} id
148 * @param {string} name
149 * @param {!WebInspector.ResourceType} type
150 * @param {string} resourceTypeName
151 * @param {boolean} allowedPerDomain
153 WebInspector.AuditRules.CombineExternalResourcesRule = function(id, name, type, resourceTypeName, allowedPerDomain)
155 WebInspector.AuditRule.call(this, id, name);
156 this._type = type;
157 this._resourceTypeName = resourceTypeName;
158 this._allowedPerDomain = allowedPerDomain;
161 WebInspector.AuditRules.CombineExternalResourcesRule.prototype = {
163 * @override
164 * @param {!WebInspector.Target} target
165 * @param {!Array.<!WebInspector.NetworkRequest>} requests
166 * @param {!WebInspector.AuditRuleResult} result
167 * @param {function(?WebInspector.AuditRuleResult)} callback
168 * @param {!WebInspector.Progress} progress
170 doRun: function(target, requests, result, callback, progress)
172 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests, [this._type], false);
173 var penalizedResourceCount = 0;
174 // TODO: refactor according to the chosen i18n approach
175 var summary = result.addChild("", true);
176 for (var domain in domainToResourcesMap) {
177 var domainResources = domainToResourcesMap[domain];
178 var extraResourceCount = domainResources.length - this._allowedPerDomain;
179 if (extraResourceCount <= 0)
180 continue;
181 penalizedResourceCount += extraResourceCount - 1;
182 summary.addChild(WebInspector.UIString("%d %s resources served from %s.", domainResources.length, this._resourceTypeName, WebInspector.AuditRuleResult.resourceDomain(domain)));
183 result.violationCount += domainResources.length;
185 if (!penalizedResourceCount) {
186 callback(null);
187 return;
190 summary.value = WebInspector.UIString("There are multiple resources served from same domain. Consider combining them into as few files as possible.");
191 callback(result);
194 __proto__: WebInspector.AuditRule.prototype
198 * @constructor
199 * @extends {WebInspector.AuditRules.CombineExternalResourcesRule}
201 WebInspector.AuditRules.CombineJsResourcesRule = function(allowedPerDomain) {
202 WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externaljs", WebInspector.UIString("Combine external JavaScript"), WebInspector.resourceTypes.Script, "JavaScript", allowedPerDomain);
205 WebInspector.AuditRules.CombineJsResourcesRule.prototype = {
206 __proto__: WebInspector.AuditRules.CombineExternalResourcesRule.prototype
210 * @constructor
211 * @extends {WebInspector.AuditRules.CombineExternalResourcesRule}
213 WebInspector.AuditRules.CombineCssResourcesRule = function(allowedPerDomain) {
214 WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externalcss", WebInspector.UIString("Combine external CSS"), WebInspector.resourceTypes.Stylesheet, "CSS", allowedPerDomain);
217 WebInspector.AuditRules.CombineCssResourcesRule.prototype = {
218 __proto__: WebInspector.AuditRules.CombineExternalResourcesRule.prototype
222 * @constructor
223 * @extends {WebInspector.AuditRule}
225 WebInspector.AuditRules.MinimizeDnsLookupsRule = function(hostCountThreshold) {
226 WebInspector.AuditRule.call(this, "network-minimizelookups", WebInspector.UIString("Minimize DNS lookups"));
227 this._hostCountThreshold = hostCountThreshold;
230 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype = {
232 * @override
233 * @param {!WebInspector.Target} target
234 * @param {!Array.<!WebInspector.NetworkRequest>} requests
235 * @param {!WebInspector.AuditRuleResult} result
236 * @param {function(?WebInspector.AuditRuleResult)} callback
237 * @param {!WebInspector.Progress} progress
239 doRun: function(target, requests, result, callback, progress)
241 var summary = result.addChild("");
242 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests, null, false);
243 for (var domain in domainToResourcesMap) {
244 if (domainToResourcesMap[domain].length > 1)
245 continue;
246 var parsedURL = domain.asParsedURL();
247 if (!parsedURL)
248 continue;
249 if (!parsedURL.host.search(WebInspector.AuditRules.IPAddressRegexp))
250 continue; // an IP address
251 summary.addSnippet(domain);
252 result.violationCount++;
254 if (!summary.children || summary.children.length <= this._hostCountThreshold) {
255 callback(null);
256 return;
259 summary.value = WebInspector.UIString("The following domains only serve one resource each. If possible, avoid the extra DNS lookups by serving these resources from existing domains.");
260 callback(result);
263 __proto__: WebInspector.AuditRule.prototype
267 * @constructor
268 * @extends {WebInspector.AuditRule}
270 WebInspector.AuditRules.ParallelizeDownloadRule = function(optimalHostnameCount, minRequestThreshold, minBalanceThreshold)
272 WebInspector.AuditRule.call(this, "network-parallelizehosts", WebInspector.UIString("Parallelize downloads across hostnames"));
273 this._optimalHostnameCount = optimalHostnameCount;
274 this._minRequestThreshold = minRequestThreshold;
275 this._minBalanceThreshold = minBalanceThreshold;
278 WebInspector.AuditRules.ParallelizeDownloadRule.prototype = {
280 * @override
281 * @param {!WebInspector.Target} target
282 * @param {!Array.<!WebInspector.NetworkRequest>} requests
283 * @param {!WebInspector.AuditRuleResult} result
284 * @param {function(?WebInspector.AuditRuleResult)} callback
285 * @param {!WebInspector.Progress} progress
287 doRun: function(target, requests, result, callback, progress)
290 * @param {string} a
291 * @param {string} b
293 function hostSorter(a, b)
295 var aCount = domainToResourcesMap[a].length;
296 var bCount = domainToResourcesMap[b].length;
297 return (aCount < bCount) ? 1 : (aCount === bCount) ? 0 : -1;
300 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(
301 requests,
302 [WebInspector.resourceTypes.Stylesheet, WebInspector.resourceTypes.Image],
303 true);
305 var hosts = [];
306 for (var url in domainToResourcesMap)
307 hosts.push(url);
309 if (!hosts.length) {
310 callback(null); // no hosts (local file or something)
311 return;
314 hosts.sort(hostSorter);
316 var optimalHostnameCount = this._optimalHostnameCount;
317 if (hosts.length > optimalHostnameCount)
318 hosts.splice(optimalHostnameCount);
320 var busiestHostResourceCount = domainToResourcesMap[hosts[0]].length;
321 var requestCountAboveThreshold = busiestHostResourceCount - this._minRequestThreshold;
322 if (requestCountAboveThreshold <= 0) {
323 callback(null);
324 return;
327 var avgResourcesPerHost = 0;
328 for (var i = 0, size = hosts.length; i < size; ++i)
329 avgResourcesPerHost += domainToResourcesMap[hosts[i]].length;
331 // Assume optimal parallelization.
332 avgResourcesPerHost /= optimalHostnameCount;
333 avgResourcesPerHost = Math.max(avgResourcesPerHost, 1);
335 var pctAboveAvg = (requestCountAboveThreshold / avgResourcesPerHost) - 1.0;
336 var minBalanceThreshold = this._minBalanceThreshold;
337 if (pctAboveAvg < minBalanceThreshold) {
338 callback(null);
339 return;
342 var requestsOnBusiestHost = domainToResourcesMap[hosts[0]];
343 var entry = result.addChild(WebInspector.UIString("This page makes %d parallelizable requests to %s. Increase download parallelization by distributing the following requests across multiple hostnames.", busiestHostResourceCount, hosts[0]), true);
344 for (var i = 0; i < requestsOnBusiestHost.length; ++i)
345 entry.addURL(requestsOnBusiestHost[i].url);
347 result.violationCount = requestsOnBusiestHost.length;
348 callback(result);
351 __proto__: WebInspector.AuditRule.prototype
355 * The reported CSS rule size is incorrect (parsed != original in WebKit),
356 * so use percentages instead, which gives a better approximation.
357 * @constructor
358 * @extends {WebInspector.AuditRule}
360 WebInspector.AuditRules.UnusedCssRule = function()
362 WebInspector.AuditRule.call(this, "page-unusedcss", WebInspector.UIString("Remove unused CSS rules"));
365 WebInspector.AuditRules.UnusedCssRule.prototype = {
367 * @override
368 * @param {!WebInspector.Target} target
369 * @param {!Array.<!WebInspector.NetworkRequest>} requests
370 * @param {!WebInspector.AuditRuleResult} result
371 * @param {function(?WebInspector.AuditRuleResult)} callback
372 * @param {!WebInspector.Progress} progress
374 doRun: function(target, requests, result, callback, progress)
376 var domModel = WebInspector.DOMModel.fromTarget(target);
377 var cssModel = WebInspector.CSSStyleModel.fromTarget(target);
378 if (!domModel || !cssModel) {
379 callback(null);
380 return;
384 * @param {!Array.<!WebInspector.AuditRules.ParsedStyleSheet>} styleSheets
386 function evalCallback(styleSheets) {
387 if (!styleSheets.length)
388 return callback(null);
390 var selectors = [];
391 var testedSelectors = {};
392 for (var i = 0; i < styleSheets.length; ++i) {
393 var styleSheet = styleSheets[i];
394 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
395 var selectorText = styleSheet.rules[curRule].selectorText;
396 if (testedSelectors[selectorText])
397 continue;
398 selectors.push(selectorText);
399 testedSelectors[selectorText] = 1;
403 var foundSelectors = {};
406 * @param {!Array.<!WebInspector.AuditRules.ParsedStyleSheet>} styleSheets
408 function selectorsCallback(styleSheets)
410 if (progress.isCanceled()) {
411 callback(null);
412 return;
415 var inlineBlockOrdinal = 0;
416 var totalStylesheetSize = 0;
417 var totalUnusedStylesheetSize = 0;
418 var summary;
420 for (var i = 0; i < styleSheets.length; ++i) {
421 var styleSheet = styleSheets[i];
422 var unusedRules = [];
423 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
424 var rule = styleSheet.rules[curRule];
425 if (!testedSelectors[rule.selectorText] || foundSelectors[rule.selectorText])
426 continue;
427 unusedRules.push(rule.selectorText);
429 totalStylesheetSize += styleSheet.rules.length;
430 totalUnusedStylesheetSize += unusedRules.length;
432 if (!unusedRules.length)
433 continue;
435 var resource = WebInspector.resourceForURL(styleSheet.sourceURL);
436 var isInlineBlock = resource && resource.request && resource.request.resourceType() === WebInspector.resourceTypes.Document;
437 var url = !isInlineBlock ? WebInspector.AuditRuleResult.linkifyDisplayName(styleSheet.sourceURL) : WebInspector.UIString("Inline block #%d", ++inlineBlockOrdinal);
438 var pctUnused = Math.round(100 * unusedRules.length / styleSheet.rules.length);
439 if (!summary)
440 summary = result.addChild("", true);
441 var entry = summary.addFormatted("%s: %d% is not used by the current page.", url, pctUnused);
443 for (var j = 0; j < unusedRules.length; ++j)
444 entry.addSnippet(unusedRules[j]);
446 result.violationCount += unusedRules.length;
449 if (!totalUnusedStylesheetSize)
450 return callback(null);
452 var totalUnusedPercent = Math.round(100 * totalUnusedStylesheetSize / totalStylesheetSize);
453 summary.value = WebInspector.UIString("%s rules (%d%) of CSS not used by the current page.", totalUnusedStylesheetSize, totalUnusedPercent);
455 callback(result);
459 * @param {?function()} boundSelectorsCallback
460 * @param {string} selector
461 * @param {?DOMAgent.NodeId} nodeId
463 function queryCallback(boundSelectorsCallback, selector, nodeId)
465 if (nodeId)
466 foundSelectors[selector] = true;
467 if (boundSelectorsCallback)
468 boundSelectorsCallback();
472 * @param {!Array.<string>} selectors
473 * @param {!WebInspector.DOMDocument} document
475 function documentLoaded(selectors, document) {
476 var pseudoSelectorRegexp = /::?(?:[\w-]+)(?:\(.*?\))?/g;
477 if (!selectors.length) {
478 selectorsCallback([]);
479 return;
481 for (var i = 0; i < selectors.length; ++i) {
482 if (progress.isCanceled()) {
483 callback(null);
484 return;
486 var effectiveSelector = selectors[i].replace(pseudoSelectorRegexp, "");
487 domModel.querySelector(document.id, effectiveSelector, queryCallback.bind(null, i === selectors.length - 1 ? selectorsCallback.bind(null, styleSheets) : null, selectors[i]));
491 domModel.requestDocument(documentLoaded.bind(null, selectors));
494 var styleSheetInfos = cssModel.allStyleSheets();
495 if (!styleSheetInfos || !styleSheetInfos.length) {
496 evalCallback([]);
497 return;
499 var styleSheetProcessor = new WebInspector.AuditRules.StyleSheetProcessor(styleSheetInfos, progress, evalCallback);
500 styleSheetProcessor.run();
503 __proto__: WebInspector.AuditRule.prototype
507 * @typedef {!{sourceURL: string, rules: !Array.<!WebInspector.CSSParser.StyleRule>}}
509 WebInspector.AuditRules.ParsedStyleSheet;
512 * @constructor
513 * @param {!Array.<!WebInspector.CSSStyleSheetHeader>} styleSheetHeaders
514 * @param {!WebInspector.Progress} progress
515 * @param {function(!Array.<!WebInspector.AuditRules.ParsedStyleSheet>)} styleSheetsParsedCallback
517 WebInspector.AuditRules.StyleSheetProcessor = function(styleSheetHeaders, progress, styleSheetsParsedCallback)
519 this._styleSheetHeaders = styleSheetHeaders;
520 this._progress = progress;
521 this._styleSheets = [];
522 this._styleSheetsParsedCallback = styleSheetsParsedCallback;
525 WebInspector.AuditRules.StyleSheetProcessor.prototype = {
526 run: function()
528 this._parser = new WebInspector.CSSParser();
529 this._processNextStyleSheet();
532 _terminateWorker: function()
534 if (this._parser) {
535 this._parser.dispose();
536 delete this._parser;
540 _finish: function()
542 this._terminateWorker();
543 this._styleSheetsParsedCallback(this._styleSheets);
546 _processNextStyleSheet: function()
548 if (!this._styleSheetHeaders.length) {
549 this._finish();
550 return;
552 this._currentStyleSheetHeader = this._styleSheetHeaders.shift();
553 this._parser.fetchAndParse(this._currentStyleSheetHeader, this._onStyleSheetParsed.bind(this));
557 * @param {!Array.<!WebInspector.CSSParser.Rule>} rules
559 _onStyleSheetParsed: function(rules)
561 if (this._progress.isCanceled()) {
562 this._finish();
563 return;
566 var styleRules = [];
567 for (var i = 0; i < rules.length; ++i) {
568 var rule = rules[i];
569 if (rule.selectorText)
570 styleRules.push(rule);
572 this._styleSheets.push({
573 sourceURL: this._currentStyleSheetHeader.sourceURL,
574 rules: styleRules
576 this._processNextStyleSheet();
581 * @constructor
582 * @extends {WebInspector.AuditRule}
584 WebInspector.AuditRules.CacheControlRule = function(id, name)
586 WebInspector.AuditRule.call(this, id, name);
589 WebInspector.AuditRules.CacheControlRule.MillisPerMonth = 1000 * 60 * 60 * 24 * 30;
591 WebInspector.AuditRules.CacheControlRule.prototype = {
593 * @override
594 * @param {!WebInspector.Target} target
595 * @param {!Array.<!WebInspector.NetworkRequest>} requests
596 * @param {!WebInspector.AuditRuleResult} result
597 * @param {function(!WebInspector.AuditRuleResult)} callback
598 * @param {!WebInspector.Progress} progress
600 doRun: function(target, requests, result, callback, progress)
602 var cacheableAndNonCacheableResources = this._cacheableAndNonCacheableResources(requests);
603 if (cacheableAndNonCacheableResources[0].length)
604 this.runChecks(cacheableAndNonCacheableResources[0], result);
605 this.handleNonCacheableResources(cacheableAndNonCacheableResources[1], result);
607 callback(result);
610 handleNonCacheableResources: function(requests, result)
614 _cacheableAndNonCacheableResources: function(requests)
616 var processedResources = [[], []];
617 for (var i = 0; i < requests.length; ++i) {
618 var request = requests[i];
619 if (!this.isCacheableResource(request))
620 continue;
621 if (this._isExplicitlyNonCacheable(request))
622 processedResources[1].push(request);
623 else
624 processedResources[0].push(request);
626 return processedResources;
629 execCheck: function(messageText, requestCheckFunction, requests, result)
631 var requestCount = requests.length;
632 var urls = [];
633 for (var i = 0; i < requestCount; ++i) {
634 if (requestCheckFunction.call(this, requests[i]))
635 urls.push(requests[i].url);
637 if (urls.length) {
638 var entry = result.addChild(messageText, true);
639 entry.addURLs(urls);
640 result.violationCount += urls.length;
645 * @param {!WebInspector.NetworkRequest} request
646 * @param {number} timeMs
647 * @return {boolean}
649 freshnessLifetimeGreaterThan: function(request, timeMs)
651 var dateHeader = this.responseHeader(request, "Date");
652 if (!dateHeader)
653 return false;
655 var dateHeaderMs = Date.parse(dateHeader);
656 if (isNaN(dateHeaderMs))
657 return false;
659 var freshnessLifetimeMs;
660 var maxAgeMatch = this.responseHeaderMatch(request, "Cache-Control", "max-age=(\\d+)");
662 if (maxAgeMatch)
663 freshnessLifetimeMs = (maxAgeMatch[1]) ? 1000 * maxAgeMatch[1] : 0;
664 else {
665 var expiresHeader = this.responseHeader(request, "Expires");
666 if (expiresHeader) {
667 var expDate = Date.parse(expiresHeader);
668 if (!isNaN(expDate))
669 freshnessLifetimeMs = expDate - dateHeaderMs;
673 return (isNaN(freshnessLifetimeMs)) ? false : freshnessLifetimeMs > timeMs;
677 * @param {!WebInspector.NetworkRequest} request
678 * @param {string} header
679 * @return {string|undefined}
681 responseHeader: function(request, header)
683 return request.responseHeaderValue(header);
687 * @param {!WebInspector.NetworkRequest} request
688 * @param {string} header
689 * @return {boolean}
691 hasResponseHeader: function(request, header)
693 return request.responseHeaderValue(header) !== undefined;
697 * @param {!WebInspector.NetworkRequest} request
698 * @return {boolean}
700 isCompressible: function(request)
702 return request.resourceType().isTextType();
706 * @param {!WebInspector.NetworkRequest} request
707 * @return {boolean}
709 isPubliclyCacheable: function(request)
711 if (this._isExplicitlyNonCacheable(request))
712 return false;
714 if (this.responseHeaderMatch(request, "Cache-Control", "public"))
715 return true;
717 return request.url.indexOf("?") === -1 && !this.responseHeaderMatch(request, "Cache-Control", "private");
721 * @param {!WebInspector.NetworkRequest} request
722 * @param {string} header
723 * @param {string} regexp
724 * @return {?Array.<string>}
726 responseHeaderMatch: function(request, header, regexp)
728 return request.responseHeaderValue(header)
729 ? request.responseHeaderValue(header).match(new RegExp(regexp, "im"))
730 : null;
734 * @param {!WebInspector.NetworkRequest} request
735 * @return {boolean}
737 hasExplicitExpiration: function(request)
739 return this.hasResponseHeader(request, "Date") &&
740 (this.hasResponseHeader(request, "Expires") || !!this.responseHeaderMatch(request, "Cache-Control", "max-age"));
744 * @param {!WebInspector.NetworkRequest} request
745 * @return {boolean}
747 _isExplicitlyNonCacheable: function(request)
749 var hasExplicitExp = this.hasExplicitExpiration(request);
750 return !!this.responseHeaderMatch(request, "Cache-Control", "(no-cache|no-store)") ||
751 !!this.responseHeaderMatch(request, "Pragma", "no-cache") ||
752 (hasExplicitExp && !this.freshnessLifetimeGreaterThan(request, 0)) ||
753 (!hasExplicitExp && !!request.url && request.url.indexOf("?") >= 0) ||
754 (!hasExplicitExp && !this.isCacheableResource(request));
758 * @param {!WebInspector.NetworkRequest} request
759 * @return {boolean}
761 isCacheableResource: function(request)
763 return request.statusCode !== undefined && WebInspector.AuditRules.CacheableResponseCodes[request.statusCode];
766 __proto__: WebInspector.AuditRule.prototype
770 * @constructor
771 * @extends {WebInspector.AuditRules.CacheControlRule}
773 WebInspector.AuditRules.BrowserCacheControlRule = function()
775 WebInspector.AuditRules.CacheControlRule.call(this, "http-browsercache", WebInspector.UIString("Leverage browser caching"));
778 WebInspector.AuditRules.BrowserCacheControlRule.prototype = {
779 handleNonCacheableResources: function(requests, result)
781 if (requests.length) {
782 var entry = result.addChild(WebInspector.UIString("The following resources are explicitly non-cacheable. Consider making them cacheable if possible:"), true);
783 result.violationCount += requests.length;
784 for (var i = 0; i < requests.length; ++i)
785 entry.addURL(requests[i].url);
789 runChecks: function(requests, result, callback)
791 this.execCheck(WebInspector.UIString("The following resources are missing a cache expiration. Resources that do not specify an expiration may not be cached by browsers:"),
792 this._missingExpirationCheck, requests, result);
793 this.execCheck(WebInspector.UIString("The following resources specify a \"Vary\" header that disables caching in most versions of Internet Explorer:"),
794 this._varyCheck, requests, result);
795 this.execCheck(WebInspector.UIString("The following cacheable resources have a short freshness lifetime:"),
796 this._oneMonthExpirationCheck, requests, result);
798 // Unable to implement the favicon check due to the WebKit limitations.
799 this.execCheck(WebInspector.UIString("To further improve cache hit rate, specify an expiration one year in the future for the following cacheable resources:"),
800 this._oneYearExpirationCheck, requests, result);
803 _missingExpirationCheck: function(request)
805 return this.isCacheableResource(request) && !this.hasResponseHeader(request, "Set-Cookie") && !this.hasExplicitExpiration(request);
808 _varyCheck: function(request)
810 var varyHeader = this.responseHeader(request, "Vary");
811 if (varyHeader) {
812 varyHeader = varyHeader.replace(/User-Agent/gi, "");
813 varyHeader = varyHeader.replace(/Accept-Encoding/gi, "");
814 varyHeader = varyHeader.replace(/[, ]*/g, "");
816 return varyHeader && varyHeader.length && this.isCacheableResource(request) && this.freshnessLifetimeGreaterThan(request, 0);
819 _oneMonthExpirationCheck: function(request)
821 return this.isCacheableResource(request) &&
822 !this.hasResponseHeader(request, "Set-Cookie") &&
823 !this.freshnessLifetimeGreaterThan(request, WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
824 this.freshnessLifetimeGreaterThan(request, 0);
827 _oneYearExpirationCheck: function(request)
829 return this.isCacheableResource(request) &&
830 !this.hasResponseHeader(request, "Set-Cookie") &&
831 !this.freshnessLifetimeGreaterThan(request, 11 * WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
832 this.freshnessLifetimeGreaterThan(request, WebInspector.AuditRules.CacheControlRule.MillisPerMonth);
835 __proto__: WebInspector.AuditRules.CacheControlRule.prototype
839 * @constructor
840 * @extends {WebInspector.AuditRule}
842 WebInspector.AuditRules.ImageDimensionsRule = function()
844 WebInspector.AuditRule.call(this, "page-imagedims", WebInspector.UIString("Specify image dimensions"));
847 WebInspector.AuditRules.ImageDimensionsRule.prototype = {
849 * @override
850 * @param {!WebInspector.Target} target
851 * @param {!Array.<!WebInspector.NetworkRequest>} requests
852 * @param {!WebInspector.AuditRuleResult} result
853 * @param {function(?WebInspector.AuditRuleResult)} callback
854 * @param {!WebInspector.Progress} progress
856 doRun: function(target, requests, result, callback, progress)
858 var domModel = WebInspector.DOMModel.fromTarget(target);
859 var cssModel = WebInspector.CSSStyleModel.fromTarget(target);
860 if (!domModel || !cssModel) {
861 callback(null);
862 return;
865 var urlToNoDimensionCount = {};
867 function doneCallback()
869 for (var url in urlToNoDimensionCount) {
870 var entry = entry || result.addChild(WebInspector.UIString("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);
871 var format = "%r";
872 if (urlToNoDimensionCount[url] > 1)
873 format += " (%d uses)";
874 entry.addFormatted(format, url, urlToNoDimensionCount[url]);
875 result.violationCount++;
877 callback(entry ? result : null);
880 function imageStylesReady(imageId, styles)
882 if (progress.isCanceled()) {
883 callback(null);
884 return;
887 const node = domModel.nodeForId(imageId);
888 var src = node.getAttribute("src");
889 if (!src.asParsedURL()) {
890 for (var frameOwnerCandidate = node; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) {
891 if (frameOwnerCandidate.baseURL) {
892 var completeSrc = WebInspector.ParsedURL.completeURL(frameOwnerCandidate.baseURL, src);
893 break;
897 if (completeSrc)
898 src = completeSrc;
900 if (styles.computedStyle.get("position") === "absolute")
901 return;
903 if (styles.attributesStyle) {
904 var widthFound = !!styles.attributesStyle.getPropertyValue("width");
905 var heightFound = !!styles.attributesStyle.getPropertyValue("height");
908 var inlineStyle = styles.inlineStyle;
909 if (inlineStyle) {
910 if (inlineStyle.getPropertyValue("width") !== "")
911 widthFound = true;
912 if (inlineStyle.getPropertyValue("height") !== "")
913 heightFound = true;
916 for (var i = styles.matchedCSSRules.length - 1; i >= 0 && !(widthFound && heightFound); --i) {
917 var style = styles.matchedCSSRules[i].style;
918 if (style.getPropertyValue("width") !== "")
919 widthFound = true;
920 if (style.getPropertyValue("height") !== "")
921 heightFound = true;
924 if (!widthFound || !heightFound) {
925 if (src in urlToNoDimensionCount)
926 ++urlToNoDimensionCount[src];
927 else
928 urlToNoDimensionCount[src] = 1;
933 * @param {!Array.<!DOMAgent.NodeId>=} nodeIds
935 function getStyles(nodeIds)
937 if (progress.isCanceled()) {
938 callback(null);
939 return;
941 var targetResult = {};
944 * @param {?WebInspector.CSSStyleModel.MatchedStyleResult} matchedStyleResult
946 function matchedCallback(matchedStyleResult)
948 if (!matchedStyleResult)
949 return;
950 targetResult.matchedCSSRules = matchedStyleResult.matchedCSSRules;
951 targetResult.inlineStyle = matchedStyleResult.inlineStyle;
952 targetResult.attributesStyle = matchedStyleResult.attributesStyle;
956 * @param {?Map.<string, string>} computedStyle
958 function computedCallback(computedStyle)
960 targetResult.computedStyle = computedStyle;
963 if (!nodeIds || !nodeIds.length)
964 doneCallback();
966 var nodePromises = [];
967 for (var i = 0; nodeIds && i < nodeIds.length; ++i) {
968 var stylePromises = [
969 cssModel.matchedStylesPromise(nodeIds[i]).then(matchedCallback),
970 cssModel.computedStylePromise(nodeIds[i]).then(computedCallback)
972 var nodePromise = Promise.all(stylePromises).then(imageStylesReady.bind(null, nodeIds[i], targetResult));
973 nodePromises.push(nodePromise);
975 Promise.all(nodePromises)
976 .catchException(null)
977 .then(doneCallback);
980 function onDocumentAvailable(root)
982 if (progress.isCanceled()) {
983 callback(null);
984 return;
986 domModel.querySelectorAll(root.id, "img[src]", getStyles);
989 if (progress.isCanceled()) {
990 callback(null);
991 return;
993 domModel.requestDocument(onDocumentAvailable);
996 __proto__: WebInspector.AuditRule.prototype
1000 * @constructor
1001 * @extends {WebInspector.AuditRule}
1003 WebInspector.AuditRules.CssInHeadRule = function()
1005 WebInspector.AuditRule.call(this, "page-cssinhead", WebInspector.UIString("Put CSS in the document head"));
1008 WebInspector.AuditRules.CssInHeadRule.prototype = {
1010 * @override
1011 * @param {!WebInspector.Target} target
1012 * @param {!Array.<!WebInspector.NetworkRequest>} requests
1013 * @param {!WebInspector.AuditRuleResult} result
1014 * @param {function(?WebInspector.AuditRuleResult)} callback
1015 * @param {!WebInspector.Progress} progress
1017 doRun: function(target, requests, result, callback, progress)
1019 var domModel = WebInspector.DOMModel.fromTarget(target);
1020 if (!domModel) {
1021 callback(null);
1022 return;
1025 function evalCallback(evalResult)
1027 if (progress.isCanceled()) {
1028 callback(null);
1029 return;
1032 if (!evalResult)
1033 return callback(null);
1035 var summary = result.addChild("");
1037 for (var url in evalResult) {
1038 var urlViolations = evalResult[url];
1039 if (urlViolations[0]) {
1040 result.addFormatted("%s style block(s) in the %r body should be moved to the document head.", urlViolations[0], url);
1041 result.violationCount += urlViolations[0];
1043 for (var i = 0; i < urlViolations[1].length; ++i)
1044 result.addFormatted("Link node %r should be moved to the document head in %r", urlViolations[1][i], url);
1045 result.violationCount += urlViolations[1].length;
1047 summary.value = WebInspector.UIString("CSS in the document body adversely impacts rendering performance.");
1048 callback(result);
1052 * @param {!WebInspector.DOMNode} root
1053 * @param {!Array.<!DOMAgent.NodeId>=} inlineStyleNodeIds
1054 * @param {!Array.<!DOMAgent.NodeId>=} nodeIds
1056 function externalStylesheetsReceived(root, inlineStyleNodeIds, nodeIds)
1058 if (progress.isCanceled()) {
1059 callback(null);
1060 return;
1063 if (!nodeIds)
1064 return;
1065 var externalStylesheetNodeIds = nodeIds;
1066 var result = null;
1067 if (inlineStyleNodeIds.length || externalStylesheetNodeIds.length) {
1068 var urlToViolationsArray = {};
1069 var externalStylesheetHrefs = [];
1070 for (var j = 0; j < externalStylesheetNodeIds.length; ++j) {
1071 var linkNode = domModel.nodeForId(externalStylesheetNodeIds[j]);
1072 var completeHref = WebInspector.ParsedURL.completeURL(linkNode.ownerDocument.baseURL, linkNode.getAttribute("href"));
1073 externalStylesheetHrefs.push(completeHref || "<empty>");
1075 urlToViolationsArray[root.documentURL] = [inlineStyleNodeIds.length, externalStylesheetHrefs];
1076 result = urlToViolationsArray;
1078 evalCallback(result);
1082 * @param {!WebInspector.DOMNode} root
1083 * @param {!Array.<!DOMAgent.NodeId>=} nodeIds
1085 function inlineStylesReceived(root, nodeIds)
1087 if (progress.isCanceled()) {
1088 callback(null);
1089 return;
1092 if (!nodeIds)
1093 return;
1094 domModel.querySelectorAll(root.id, "body link[rel~='stylesheet'][href]", externalStylesheetsReceived.bind(null, root, nodeIds));
1098 * @param {!WebInspector.DOMNode} root
1100 function onDocumentAvailable(root)
1102 if (progress.isCanceled()) {
1103 callback(null);
1104 return;
1107 domModel.querySelectorAll(root.id, "body style", inlineStylesReceived.bind(null, root));
1110 domModel.requestDocument(onDocumentAvailable);
1113 __proto__: WebInspector.AuditRule.prototype
1117 * @constructor
1118 * @extends {WebInspector.AuditRule}
1120 WebInspector.AuditRules.StylesScriptsOrderRule = function()
1122 WebInspector.AuditRule.call(this, "page-stylescriptorder", WebInspector.UIString("Optimize the order of styles and scripts"));
1125 WebInspector.AuditRules.StylesScriptsOrderRule.prototype = {
1127 * @override
1128 * @param {!WebInspector.Target} target
1129 * @param {!Array.<!WebInspector.NetworkRequest>} requests
1130 * @param {!WebInspector.AuditRuleResult} result
1131 * @param {function(?WebInspector.AuditRuleResult)} callback
1132 * @param {!WebInspector.Progress} progress
1134 doRun: function(target, requests, result, callback, progress)
1136 var domModel = WebInspector.DOMModel.fromTarget(target);
1137 if (!domModel) {
1138 callback(null);
1139 return;
1142 function evalCallback(resultValue)
1144 if (progress.isCanceled()) {
1145 callback(null);
1146 return;
1149 if (!resultValue)
1150 return callback(null);
1152 var lateCssUrls = resultValue[0];
1153 var cssBeforeInlineCount = resultValue[1];
1155 if (lateCssUrls.length) {
1156 var entry = result.addChild(WebInspector.UIString("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);
1157 entry.addURLs(lateCssUrls);
1158 result.violationCount += lateCssUrls.length;
1161 if (cssBeforeInlineCount) {
1162 result.addChild(WebInspector.UIString(" %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"));
1163 result.violationCount += cssBeforeInlineCount;
1165 callback(result);
1169 * @param {!Array.<!DOMAgent.NodeId>} lateStyleIds
1170 * @param {!Array.<!DOMAgent.NodeId>=} nodeIds
1172 function cssBeforeInlineReceived(lateStyleIds, nodeIds)
1174 if (progress.isCanceled()) {
1175 callback(null);
1176 return;
1179 if (!nodeIds)
1180 return;
1182 var cssBeforeInlineCount = nodeIds.length;
1183 var result = null;
1184 if (lateStyleIds.length || cssBeforeInlineCount) {
1185 var lateStyleUrls = [];
1186 for (var i = 0; i < lateStyleIds.length; ++i) {
1187 var lateStyleNode = domModel.nodeForId(lateStyleIds[i]);
1188 var completeHref = WebInspector.ParsedURL.completeURL(lateStyleNode.ownerDocument.baseURL, lateStyleNode.getAttribute("href"));
1189 lateStyleUrls.push(completeHref || "<empty>");
1191 result = [ lateStyleUrls, cssBeforeInlineCount ];
1194 evalCallback(result);
1198 * @param {!WebInspector.DOMDocument} root
1199 * @param {!Array.<!DOMAgent.NodeId>=} nodeIds
1201 function lateStylesReceived(root, nodeIds)
1203 if (progress.isCanceled()) {
1204 callback(null);
1205 return;
1208 if (!nodeIds)
1209 return;
1211 domModel.querySelectorAll(root.id, "head link[rel~='stylesheet'][href] ~ script:not([src])", cssBeforeInlineReceived.bind(null, nodeIds));
1215 * @param {!WebInspector.DOMDocument} root
1217 function onDocumentAvailable(root)
1219 if (progress.isCanceled()) {
1220 callback(null);
1221 return;
1224 domModel.querySelectorAll(root.id, "head script[src] ~ link[rel~='stylesheet'][href]", lateStylesReceived.bind(null, root));
1227 domModel.requestDocument(onDocumentAvailable);
1230 __proto__: WebInspector.AuditRule.prototype
1234 * @constructor
1235 * @extends {WebInspector.AuditRule}
1237 WebInspector.AuditRules.CSSRuleBase = function(id, name)
1239 WebInspector.AuditRule.call(this, id, name);
1242 WebInspector.AuditRules.CSSRuleBase.prototype = {
1244 * @override
1245 * @param {!WebInspector.Target} target
1246 * @param {!Array.<!WebInspector.NetworkRequest>} requests
1247 * @param {!WebInspector.AuditRuleResult} result
1248 * @param {function(?WebInspector.AuditRuleResult)} callback
1249 * @param {!WebInspector.Progress} progress
1251 doRun: function(target, requests, result, callback, progress)
1253 var cssModel = WebInspector.CSSStyleModel.fromTarget(target);
1254 if (!cssModel) {
1255 callback(null);
1256 return;
1259 var headers = cssModel.allStyleSheets();
1260 if (!headers.length) {
1261 callback(null);
1262 return;
1264 var activeHeaders = [];
1265 for (var i = 0; i < headers.length; ++i) {
1266 if (!headers[i].disabled)
1267 activeHeaders.push(headers[i]);
1270 var styleSheetProcessor = new WebInspector.AuditRules.StyleSheetProcessor(activeHeaders, progress, this._styleSheetsLoaded.bind(this, result, callback, progress));
1271 styleSheetProcessor.run();
1275 * @param {!WebInspector.AuditRuleResult} result
1276 * @param {function(!WebInspector.AuditRuleResult)} callback
1277 * @param {!WebInspector.Progress} progress
1278 * @param {!Array.<!WebInspector.AuditRules.ParsedStyleSheet>} styleSheets
1280 _styleSheetsLoaded: function(result, callback, progress, styleSheets)
1282 for (var i = 0; i < styleSheets.length; ++i)
1283 this._visitStyleSheet(styleSheets[i], result);
1284 callback(result);
1288 * @param {!WebInspector.AuditRules.ParsedStyleSheet} styleSheet
1289 * @param {!WebInspector.AuditRuleResult} result
1291 _visitStyleSheet: function(styleSheet, result)
1293 this.visitStyleSheet(styleSheet, result);
1295 for (var i = 0; i < styleSheet.rules.length; ++i)
1296 this._visitRule(styleSheet, styleSheet.rules[i], result);
1298 this.didVisitStyleSheet(styleSheet, result);
1302 * @param {!WebInspector.AuditRules.ParsedStyleSheet} styleSheet
1303 * @param {!WebInspector.CSSParser.StyleRule} rule
1304 * @param {!WebInspector.AuditRuleResult} result
1306 _visitRule: function(styleSheet, rule, result)
1308 this.visitRule(styleSheet, rule, result);
1309 var allProperties = rule.properties;
1310 for (var i = 0; i < allProperties.length; ++i)
1311 this.visitProperty(styleSheet, rule, allProperties[i], result);
1312 this.didVisitRule(styleSheet, rule, result);
1316 * @param {!WebInspector.AuditRules.ParsedStyleSheet} styleSheet
1317 * @param {!WebInspector.AuditRuleResult} result
1319 visitStyleSheet: function(styleSheet, result)
1321 // Subclasses can implement.
1325 * @param {!WebInspector.AuditRules.ParsedStyleSheet} styleSheet
1326 * @param {!WebInspector.AuditRuleResult} result
1328 didVisitStyleSheet: function(styleSheet, result)
1330 // Subclasses can implement.
1334 * @param {!WebInspector.AuditRules.ParsedStyleSheet} styleSheet
1335 * @param {!WebInspector.CSSParser.StyleRule} rule
1336 * @param {!WebInspector.AuditRuleResult} result
1338 visitRule: function(styleSheet, rule, result)
1340 // Subclasses can implement.
1344 * @param {!WebInspector.AuditRules.ParsedStyleSheet} styleSheet
1345 * @param {!WebInspector.CSSParser.StyleRule} rule
1346 * @param {!WebInspector.AuditRuleResult} result
1348 didVisitRule: function(styleSheet, rule, result)
1350 // Subclasses can implement.
1354 * @param {!WebInspector.AuditRules.ParsedStyleSheet} styleSheet
1355 * @param {!WebInspector.CSSParser.StyleRule} rule
1356 * @param {!WebInspector.CSSParser.Property} property
1357 * @param {!WebInspector.AuditRuleResult} result
1359 visitProperty: function(styleSheet, rule, property, result)
1361 // Subclasses can implement.
1364 __proto__: WebInspector.AuditRule.prototype
1368 * @constructor
1369 * @extends {WebInspector.AuditRules.CSSRuleBase}
1371 WebInspector.AuditRules.VendorPrefixedCSSProperties = function()
1373 WebInspector.AuditRules.CSSRuleBase.call(this, "page-vendorprefixedcss", WebInspector.UIString("Use normal CSS property names instead of vendor-prefixed ones"));
1374 this._webkitPrefix = "-webkit-";
1377 WebInspector.AuditRules.VendorPrefixedCSSProperties.supportedProperties = [
1378 "background-clip", "background-origin", "background-size",
1379 "border-radius", "border-bottom-left-radius", "border-bottom-right-radius", "border-top-left-radius", "border-top-right-radius",
1380 "box-shadow", "box-sizing", "opacity", "text-shadow"
1381 ].keySet();
1383 WebInspector.AuditRules.VendorPrefixedCSSProperties.prototype = {
1385 * @override
1386 * @param {!WebInspector.AuditRules.ParsedStyleSheet} styleSheet
1388 didVisitStyleSheet: function(styleSheet)
1390 delete this._styleSheetResult;
1394 * @override
1396 visitRule: function()
1398 this._mentionedProperties = {};
1401 didVisitRule: function()
1403 delete this._ruleResult;
1404 delete this._mentionedProperties;
1408 * @override
1409 * @param {!WebInspector.AuditRules.ParsedStyleSheet} styleSheet
1410 * @param {!WebInspector.CSSParser.StyleRule} rule
1411 * @param {!WebInspector.CSSParser.Property} property
1412 * @param {!WebInspector.AuditRuleResult} result
1414 visitProperty: function(styleSheet, rule, property, result)
1416 if (!property.name.startsWith(this._webkitPrefix))
1417 return;
1419 var normalPropertyName = property.name.substring(this._webkitPrefix.length).toLowerCase(); // Start just after the "-webkit-" prefix.
1420 if (WebInspector.AuditRules.VendorPrefixedCSSProperties.supportedProperties[normalPropertyName] && !this._mentionedProperties[normalPropertyName]) {
1421 this._mentionedProperties[normalPropertyName] = true;
1422 if (!this._styleSheetResult)
1423 this._styleSheetResult = result.addChild(styleSheet.sourceURL ? WebInspector.linkifyResourceAsNode(styleSheet.sourceURL) : WebInspector.UIString("<unknown>"));
1424 if (!this._ruleResult) {
1425 var anchor = WebInspector.linkifyURLAsNode(styleSheet.sourceURL, rule.selectorText);
1426 anchor.lineNumber = rule.lineNumber;
1427 this._ruleResult = this._styleSheetResult.addChild(anchor);
1429 ++result.violationCount;
1430 this._ruleResult.addSnippet(WebInspector.UIString("\"%s%s\" is used, but \"%s\" is supported.", this._webkitPrefix, normalPropertyName, normalPropertyName));
1434 __proto__: WebInspector.AuditRules.CSSRuleBase.prototype
1438 * @constructor
1439 * @extends {WebInspector.AuditRule}
1441 WebInspector.AuditRules.CookieRuleBase = function(id, name)
1443 WebInspector.AuditRule.call(this, id, name);
1446 WebInspector.AuditRules.CookieRuleBase.prototype = {
1448 * @override
1449 * @param {!WebInspector.Target} target
1450 * @param {!Array.<!WebInspector.NetworkRequest>} requests
1451 * @param {!WebInspector.AuditRuleResult} result
1452 * @param {function(!WebInspector.AuditRuleResult)} callback
1453 * @param {!WebInspector.Progress} progress
1455 doRun: function(target, requests, result, callback, progress)
1457 var self = this;
1458 function resultCallback(receivedCookies)
1460 if (progress.isCanceled()) {
1461 callback(result);
1462 return;
1465 self.processCookies(receivedCookies, requests, result);
1466 callback(result);
1469 WebInspector.Cookies.getCookiesAsync(resultCallback);
1472 mapResourceCookies: function(requestsByDomain, allCookies, callback)
1474 for (var i = 0; i < allCookies.length; ++i) {
1475 for (var requestDomain in requestsByDomain) {
1476 if (WebInspector.Cookies.cookieDomainMatchesResourceDomain(allCookies[i].domain(), requestDomain))
1477 this._callbackForResourceCookiePairs(requestsByDomain[requestDomain], allCookies[i], callback);
1482 _callbackForResourceCookiePairs: function(requests, cookie, callback)
1484 if (!requests)
1485 return;
1486 for (var i = 0; i < requests.length; ++i) {
1487 if (WebInspector.Cookies.cookieMatchesResourceURL(cookie, requests[i].url))
1488 callback(requests[i], cookie);
1492 __proto__: WebInspector.AuditRule.prototype
1496 * @constructor
1497 * @extends {WebInspector.AuditRules.CookieRuleBase}
1499 WebInspector.AuditRules.CookieSizeRule = function(avgBytesThreshold)
1501 WebInspector.AuditRules.CookieRuleBase.call(this, "http-cookiesize", WebInspector.UIString("Minimize cookie size"));
1502 this._avgBytesThreshold = avgBytesThreshold;
1503 this._maxBytesThreshold = 1000;
1506 WebInspector.AuditRules.CookieSizeRule.prototype = {
1507 _average: function(cookieArray)
1509 var total = 0;
1510 for (var i = 0; i < cookieArray.length; ++i)
1511 total += cookieArray[i].size();
1512 return cookieArray.length ? Math.round(total / cookieArray.length) : 0;
1515 _max: function(cookieArray)
1517 var result = 0;
1518 for (var i = 0; i < cookieArray.length; ++i)
1519 result = Math.max(cookieArray[i].size(), result);
1520 return result;
1523 processCookies: function(allCookies, requests, result)
1525 function maxSizeSorter(a, b)
1527 return b.maxCookieSize - a.maxCookieSize;
1530 function avgSizeSorter(a, b)
1532 return b.avgCookieSize - a.avgCookieSize;
1535 var cookiesPerResourceDomain = {};
1537 function collectorCallback(request, cookie)
1539 var cookies = cookiesPerResourceDomain[request.parsedURL.host];
1540 if (!cookies) {
1541 cookies = [];
1542 cookiesPerResourceDomain[request.parsedURL.host] = cookies;
1544 cookies.push(cookie);
1547 if (!allCookies.length)
1548 return;
1550 var sortedCookieSizes = [];
1552 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests,
1553 null,
1554 true);
1555 this.mapResourceCookies(domainToResourcesMap, allCookies, collectorCallback);
1557 for (var requestDomain in cookiesPerResourceDomain) {
1558 var cookies = cookiesPerResourceDomain[requestDomain];
1559 sortedCookieSizes.push({
1560 domain: requestDomain,
1561 avgCookieSize: this._average(cookies),
1562 maxCookieSize: this._max(cookies)
1565 var avgAllCookiesSize = this._average(allCookies);
1567 var hugeCookieDomains = [];
1568 sortedCookieSizes.sort(maxSizeSorter);
1570 for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
1571 var maxCookieSize = sortedCookieSizes[i].maxCookieSize;
1572 if (maxCookieSize > this._maxBytesThreshold)
1573 hugeCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(sortedCookieSizes[i].domain) + ": " + Number.bytesToString(maxCookieSize));
1576 var bigAvgCookieDomains = [];
1577 sortedCookieSizes.sort(avgSizeSorter);
1578 for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
1579 var domain = sortedCookieSizes[i].domain;
1580 var avgCookieSize = sortedCookieSizes[i].avgCookieSize;
1581 if (avgCookieSize > this._avgBytesThreshold && avgCookieSize < this._maxBytesThreshold)
1582 bigAvgCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(domain) + ": " + Number.bytesToString(avgCookieSize));
1584 result.addChild(WebInspector.UIString("The average cookie size for all requests on this page is %s", Number.bytesToString(avgAllCookiesSize)));
1586 if (hugeCookieDomains.length) {
1587 var entry = result.addChild(WebInspector.UIString("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);
1588 entry.addURLs(hugeCookieDomains);
1589 result.violationCount += hugeCookieDomains.length;
1592 if (bigAvgCookieDomains.length) {
1593 var entry = result.addChild(WebInspector.UIString("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);
1594 entry.addURLs(bigAvgCookieDomains);
1595 result.violationCount += bigAvgCookieDomains.length;
1599 __proto__: WebInspector.AuditRules.CookieRuleBase.prototype
1603 * @constructor
1604 * @extends {WebInspector.AuditRules.CookieRuleBase}
1606 WebInspector.AuditRules.StaticCookielessRule = function(minResources)
1608 WebInspector.AuditRules.CookieRuleBase.call(this, "http-staticcookieless", WebInspector.UIString("Serve static content from a cookieless domain"));
1609 this._minResources = minResources;
1612 WebInspector.AuditRules.StaticCookielessRule.prototype = {
1613 processCookies: function(allCookies, requests, result)
1615 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests,
1616 [WebInspector.resourceTypes.Stylesheet,
1617 WebInspector.resourceTypes.Image],
1618 true);
1619 var totalStaticResources = 0;
1620 for (var domain in domainToResourcesMap)
1621 totalStaticResources += domainToResourcesMap[domain].length;
1622 if (totalStaticResources < this._minResources)
1623 return;
1624 var matchingResourceData = {};
1625 this.mapResourceCookies(domainToResourcesMap, allCookies, this._collectorCallback.bind(this, matchingResourceData));
1627 var badUrls = [];
1628 var cookieBytes = 0;
1629 for (var url in matchingResourceData) {
1630 badUrls.push(url);
1631 cookieBytes += matchingResourceData[url];
1633 if (badUrls.length < this._minResources)
1634 return;
1636 var entry = result.addChild(WebInspector.UIString("%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);
1637 entry.addURLs(badUrls);
1638 result.violationCount = badUrls.length;
1641 _collectorCallback: function(matchingResourceData, request, cookie)
1643 matchingResourceData[request.url] = (matchingResourceData[request.url] || 0) + cookie.size();
1646 __proto__: WebInspector.AuditRules.CookieRuleBase.prototype