2 * Copyright (C) 2010 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 WebInspector.AuditRules.IPAddressRegexp = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
33 WebInspector.AuditRules.CacheableResponseCodes =
42 304: true // Underlying resource is cacheable
45 WebInspector.AuditRules.getDomainToResourcesMap = function(resources, types, needFullResources)
47 var domainToResourcesMap = {};
48 for (var i = 0, size = resources.length; i < size; ++i) {
49 var resource = resources[i];
50 if (types && types.indexOf(resource.type) === -1)
52 var parsedURL = resource.url.asParsedURL();
55 var domain = parsedURL.host;
56 var domainResources = domainToResourcesMap[domain];
57 if (domainResources === undefined) {
59 domainToResourcesMap[domain] = domainResources;
61 domainResources.push(needFullResources ? resource : resource.url);
63 return domainToResourcesMap;
66 WebInspector.AuditRules.evaluateInTargetWindow = function(func, args, callback)
68 InjectedScriptAccess.getDefault().evaluateOnSelf(func.toString(), args, callback);
72 WebInspector.AuditRules.GzipRule = function()
74 WebInspector.AuditRule.call(this, "network-gzip", "Enable gzip compression");
77 WebInspector.AuditRules.GzipRule.prototype = {
78 doRun: function(resources, result, callback)
81 var compressedSize = 0;
82 var candidateSize = 0;
83 var summary = result.addChild("", true);
84 for (var i = 0, length = resources.length; i < length; ++i) {
85 var resource = resources[i];
86 if (this._shouldCompress(resource)) {
87 var size = resource.resourceSize;
88 candidateSize += size;
89 if (this._isCompressed(resource)) {
90 compressedSize += size;
93 var savings = 2 * size / 3;
94 totalSavings += savings;
95 summary.addChild(String.sprintf("%s could save ~%s", WebInspector.AuditRuleResult.linkifyDisplayName(resource.url), Number.bytesToString(savings)));
96 result.violationCount++;
100 return callback(null);
101 summary.value = String.sprintf("Compressing the following resources with gzip could reduce their transfer size by about two thirds (~%s):", Number.bytesToString(totalSavings));
105 _isCompressed: function(resource)
107 var encoding = resource.responseHeaders["Content-Encoding"];
108 return encoding === "gzip" || encoding === "deflate";
111 _shouldCompress: function(resource)
113 return WebInspector.Resource.Type.isTextType(resource.type) && resource.domain && resource.resourceSize !== undefined && resource.resourceSize > 150;
117 WebInspector.AuditRules.GzipRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
120 WebInspector.AuditRules.CombineExternalResourcesRule = function(id, name, type, resourceTypeName, allowedPerDomain)
122 WebInspector.AuditRule.call(this, id, name);
124 this._resourceTypeName = resourceTypeName;
125 this._allowedPerDomain = allowedPerDomain;
128 WebInspector.AuditRules.CombineExternalResourcesRule.prototype = {
129 doRun: function(resources, result, callback)
131 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, [this._type]);
132 var penalizedResourceCount = 0;
133 // TODO: refactor according to the chosen i18n approach
134 var summary = result.addChild("", true);
135 for (var domain in domainToResourcesMap) {
136 var domainResources = domainToResourcesMap[domain];
137 var extraResourceCount = domainResources.length - this._allowedPerDomain;
138 if (extraResourceCount <= 0)
140 penalizedResourceCount += extraResourceCount - 1;
141 summary.addChild(String.sprintf("%d %s resources served from %s.", domainResources.length, this._resourceTypeName, WebInspector.AuditRuleResult.resourceDomain(domain)));
142 result.violationCount += domainResources.length;
144 if (!penalizedResourceCount)
145 return callback(null);
147 summary.value = "There are multiple resources served from same domain. Consider combining them into as few files as possible.";
152 WebInspector.AuditRules.CombineExternalResourcesRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
155 WebInspector.AuditRules.CombineJsResourcesRule = function(allowedPerDomain) {
156 WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externaljs", "Combine external JavaScript", WebInspector.Resource.Type.Script, "JavaScript", allowedPerDomain);
159 WebInspector.AuditRules.CombineJsResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype;
162 WebInspector.AuditRules.CombineCssResourcesRule = function(allowedPerDomain) {
163 WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externalcss", "Combine external CSS", WebInspector.Resource.Type.Stylesheet, "CSS", allowedPerDomain);
166 WebInspector.AuditRules.CombineCssResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype;
169 WebInspector.AuditRules.MinimizeDnsLookupsRule = function(hostCountThreshold) {
170 WebInspector.AuditRule.call(this, "network-minimizelookups", "Minimize DNS lookups");
171 this._hostCountThreshold = hostCountThreshold;
174 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype = {
175 doRun: function(resources, result, callback)
177 var summary = result.addChild("");
178 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, undefined);
179 for (var domain in domainToResourcesMap) {
180 if (domainToResourcesMap[domain].length > 1)
182 var parsedURL = domain.asParsedURL();
185 if (!parsedURL.host.search(WebInspector.AuditRules.IPAddressRegexp))
186 continue; // an IP address
187 summary.addSnippet(match[2]);
188 result.violationCount++;
190 if (!summary.children || summary.children.length <= this._hostCountThreshold)
191 return callback(null);
193 summary.value = "The following domains only serve one resource each. If possible, avoid the extra DNS lookups by serving these resources from existing domains.";
198 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
201 WebInspector.AuditRules.ParallelizeDownloadRule = function(optimalHostnameCount, minRequestThreshold, minBalanceThreshold)
203 WebInspector.AuditRule.call(this, "network-parallelizehosts", "Parallelize downloads across hostnames");
204 this._optimalHostnameCount = optimalHostnameCount;
205 this._minRequestThreshold = minRequestThreshold;
206 this._minBalanceThreshold = minBalanceThreshold;
210 WebInspector.AuditRules.ParallelizeDownloadRule.prototype = {
211 doRun: function(resources, result, callback)
213 function hostSorter(a, b)
215 var aCount = domainToResourcesMap[a].length;
216 var bCount = domainToResourcesMap[b].length;
217 return (aCount < bCount) ? 1 : (aCount == bCount) ? 0 : -1;
220 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(
222 [WebInspector.Resource.Type.Stylesheet, WebInspector.Resource.Type.Image],
226 for (var url in domainToResourcesMap)
230 return callback(null); // no hosts (local file or something)
232 hosts.sort(hostSorter);
234 var optimalHostnameCount = this._optimalHostnameCount;
235 if (hosts.length > optimalHostnameCount)
236 hosts.splice(optimalHostnameCount);
238 var busiestHostResourceCount = domainToResourcesMap[hosts[0]].length;
239 var resourceCountAboveThreshold = busiestHostResourceCount - this._minRequestThreshold;
240 if (resourceCountAboveThreshold <= 0)
241 return callback(null);
243 var avgResourcesPerHost = 0;
244 for (var i = 0, size = hosts.length; i < size; ++i)
245 avgResourcesPerHost += domainToResourcesMap[hosts[i]].length;
247 // Assume optimal parallelization.
248 avgResourcesPerHost /= optimalHostnameCount;
249 avgResourcesPerHost = Math.max(avgResourcesPerHost, 1);
251 var pctAboveAvg = (resourceCountAboveThreshold / avgResourcesPerHost) - 1.0;
252 var minBalanceThreshold = this._minBalanceThreshold;
253 if (pctAboveAvg < minBalanceThreshold)
254 return callback(null);
256 var resourcesOnBusiestHost = domainToResourcesMap[hosts[0]];
257 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);
258 for (var i = 0; i < resourcesOnBusiestHost.length; ++i)
259 entry.addURL(resourcesOnBusiestHost[i].url);
261 result.violationCount = resourcesOnBusiestHost.length;
266 WebInspector.AuditRules.ParallelizeDownloadRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
269 // The reported CSS rule size is incorrect (parsed != original in WebKit),
270 // so use percentages instead, which gives a better approximation.
271 WebInspector.AuditRules.UnusedCssRule = function()
273 WebInspector.AuditRule.call(this, "page-unusedcss", "Remove unused CSS rules");
276 WebInspector.AuditRules.UnusedCssRule.prototype = {
277 doRun: function(resources, result, callback)
281 function evalCallback(styleSheets) {
282 if (!styleSheets.length)
283 return callback(null);
285 var pseudoSelectorRegexp = /:hover|:link|:active|:visited|:focus|:before|:after/;
287 var testedSelectors = {};
288 for (var i = 0; i < styleSheets.length; ++i) {
289 var styleSheet = styleSheets[i];
290 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
291 var selectorText = styleSheet.rules[curRule].selectorText;
292 if (selectorText.match(pseudoSelectorRegexp) || testedSelectors[selectorText])
294 selectors.push(selectorText);
295 testedSelectors[selectorText] = 1;
299 function selectorsCallback(callback, styleSheets, testedSelectors, foundSelectors)
301 var inlineBlockOrdinal = 0;
302 var totalStylesheetSize = 0;
303 var totalUnusedStylesheetSize = 0;
306 for (var i = 0; i < styleSheets.length; ++i) {
307 var styleSheet = styleSheets[i];
308 var stylesheetSize = 0;
309 var unusedStylesheetSize = 0;
310 var unusedRules = [];
311 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
312 var rule = styleSheet.rules[curRule];
313 // Exact computation whenever source ranges are available.
314 var textLength = (rule.selectorRange && rule.style.properties.endOffset) ? rule.style.properties.endOffset - rule.selectorRange.start + 1 : 0;
315 if (!textLength && rule.style.cssText)
316 textLength = rule.style.cssText.length + rule.selectorText.length;
317 stylesheetSize += textLength;
318 if (!testedSelectors[rule.selectorText] || foundSelectors[rule.selectorText])
320 unusedStylesheetSize += textLength;
321 unusedRules.push(rule.selectorText);
323 totalStylesheetSize += stylesheetSize;
324 totalUnusedStylesheetSize += unusedStylesheetSize;
326 if (!unusedRules.length)
329 var resource = WebInspector.resourceTreeModel.resourceForURL(styleSheet.sourceURL);
330 var isInlineBlock = resource && resource.type == WebInspector.Resource.Type.Document;
331 var url = !isInlineBlock ? WebInspector.AuditRuleResult.linkifyDisplayName(styleSheet.sourceURL) : String.sprintf("Inline block #%d", ++inlineBlockOrdinal);
332 var pctUnused = Math.round(100 * unusedStylesheetSize / stylesheetSize);
334 summary = result.addChild("", true);
335 var entry = summary.addChild(String.sprintf("%s: %s (%d%%) is not used by the current page.", url, Number.bytesToString(unusedStylesheetSize), pctUnused));
337 for (var j = 0; j < unusedRules.length; ++j)
338 entry.addSnippet(unusedRules[j]);
340 result.violationCount += unusedRules.length;
343 if (!totalUnusedStylesheetSize)
344 return callback(null);
346 var totalUnusedPercent = Math.round(100 * totalUnusedStylesheetSize / totalStylesheetSize);
347 summary.value = String.sprintf("%s (%d%%) of CSS is not used by the current page.", Number.bytesToString(totalUnusedStylesheetSize), totalUnusedPercent);
352 function routine(selectorArray)
355 for (var i = 0; i < selectorArray.length; ++i) {
357 if (document.querySelector(selectorArray[i]))
358 result[selectorArray[i]] = true;
360 // Ignore and mark as unused.
366 WebInspector.AuditRules.evaluateInTargetWindow(routine, [selectors], selectorsCallback.bind(null, callback, styleSheets, testedSelectors));
369 function styleSheetCallback(styleSheets, continuation, styleSheet)
372 styleSheets.push(styleSheet);
374 continuation(styleSheets);
377 function allStylesCallback(styleSheetIds)
379 if (!styleSheetIds || !styleSheetIds.length)
380 return evalCallback([]);
381 var styleSheets = [];
382 for (var i = 0; i < styleSheetIds.length; ++i)
383 WebInspector.CSSStyleSheet.createForId(styleSheetIds[i], styleSheetCallback.bind(null, styleSheets, i == styleSheetIds.length - 1 ? evalCallback : null));
386 InspectorBackend.getAllStyles2(allStylesCallback);
390 WebInspector.AuditRules.UnusedCssRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
393 WebInspector.AuditRules.CacheControlRule = function(id, name)
395 WebInspector.AuditRule.call(this, id, name);
398 WebInspector.AuditRules.CacheControlRule.MillisPerMonth = 1000 * 60 * 60 * 24 * 30;
400 WebInspector.AuditRules.CacheControlRule.prototype = {
402 doRun: function(resources, result, callback)
404 var cacheableAndNonCacheableResources = this._cacheableAndNonCacheableResources(resources);
405 if (cacheableAndNonCacheableResources[0].length)
406 this.runChecks(cacheableAndNonCacheableResources[0], result);
407 this.handleNonCacheableResources(cacheableAndNonCacheableResources[1], result);
412 handleNonCacheableResources: function()
416 _cacheableAndNonCacheableResources: function(resources)
418 var processedResources = [[], []];
419 for (var i = 0; i < resources.length; ++i) {
420 var resource = resources[i];
421 if (!this.isCacheableResource(resource))
423 if (this._isExplicitlyNonCacheable(resource))
424 processedResources[1].push(resource);
426 processedResources[0].push(resource);
428 return processedResources;
431 execCheck: function(messageText, resourceCheckFunction, resources, result)
433 var resourceCount = resources.length;
435 for (var i = 0; i < resourceCount; ++i) {
436 if (resourceCheckFunction.call(this, resources[i]))
437 urls.push(resources[i].url);
440 var entry = result.addChild(messageText, true);
442 result.violationCount += urls.length;
446 freshnessLifetimeGreaterThan: function(resource, timeMs)
448 var dateHeader = this.responseHeader(resource, "Date");
452 var dateHeaderMs = Date.parse(dateHeader);
453 if (isNaN(dateHeaderMs))
456 var freshnessLifetimeMs;
457 var maxAgeMatch = this.responseHeaderMatch(resource, "Cache-Control", "max-age=(\\d+)");
460 freshnessLifetimeMs = (maxAgeMatch[1]) ? 1000 * maxAgeMatch[1] : 0;
462 var expiresHeader = this.responseHeader(resource, "Expires");
464 var expDate = Date.parse(expiresHeader);
466 freshnessLifetimeMs = expDate - dateHeaderMs;
470 return (isNaN(freshnessLifetimeMs)) ? false : freshnessLifetimeMs > timeMs;
473 responseHeader: function(resource, header)
475 return resource.responseHeaders[header];
478 hasResponseHeader: function(resource, header)
480 return resource.responseHeaders[header] !== undefined;
483 isCompressible: function(resource)
485 return WebInspector.Resource.Type.isTextType(resource.type);
488 isPubliclyCacheable: function(resource)
490 if (this._isExplicitlyNonCacheable(resource))
493 if (this.responseHeaderMatch(resource, "Cache-Control", "public"))
496 return resource.url.indexOf("?") == -1 && !this.responseHeaderMatch(resource, "Cache-Control", "private");
499 responseHeaderMatch: function(resource, header, regexp)
501 return resource.responseHeaders[header]
502 ? resource.responseHeaders[header].match(new RegExp(regexp, "im"))
506 hasExplicitExpiration: function(resource)
508 return this.hasResponseHeader(resource, "Date") &&
509 (this.hasResponseHeader(resource, "Expires") || this.responseHeaderMatch(resource, "Cache-Control", "max-age"));
512 _isExplicitlyNonCacheable: function(resource)
514 var hasExplicitExp = this.hasExplicitExpiration(resource);
515 return this.responseHeaderMatch(resource, "Cache-Control", "(no-cache|no-store|must-revalidate)") ||
516 this.responseHeaderMatch(resource, "Pragma", "no-cache") ||
517 (hasExplicitExp && !this.freshnessLifetimeGreaterThan(resource, 0)) ||
518 (!hasExplicitExp && resource.url && resource.url.indexOf("?") >= 0) ||
519 (!hasExplicitExp && !this.isCacheableResource(resource));
522 isCacheableResource: function(resource)
524 return resource.statusCode !== undefined && WebInspector.AuditRules.CacheableResponseCodes[resource.statusCode];
528 WebInspector.AuditRules.CacheControlRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
531 WebInspector.AuditRules.BrowserCacheControlRule = function()
533 WebInspector.AuditRules.CacheControlRule.call(this, "http-browsercache", "Leverage browser caching");
536 WebInspector.AuditRules.BrowserCacheControlRule.prototype = {
537 handleNonCacheableResources: function(resources, result)
539 if (resources.length) {
540 var entry = result.addChild("The following resources are explicitly non-cacheable. Consider making them cacheable if possible:", true);
541 result.violationCount += resources.length;
542 for (var i = 0; i < resources.length; ++i)
543 entry.addURL(resources[i].url);
547 runChecks: function(resources, result, callback)
549 this.execCheck("The following resources are missing a cache expiration. Resources that do not specify an expiration may not be cached by browsers:",
550 this._missingExpirationCheck, resources, result);
551 this.execCheck("The following resources specify a \"Vary\" header that disables caching in most versions of Internet Explorer:",
552 this._varyCheck, resources, result);
553 this.execCheck("The following cacheable resources have a short freshness lifetime:",
554 this._oneMonthExpirationCheck, resources, result);
556 // Unable to implement the favicon check due to the WebKit limitations.
557 this.execCheck("To further improve cache hit rate, specify an expiration one year in the future for the following cacheable resources:",
558 this._oneYearExpirationCheck, resources, result);
561 _missingExpirationCheck: function(resource)
563 return this.isCacheableResource(resource) && !this.hasResponseHeader(resource, "Set-Cookie") && !this.hasExplicitExpiration(resource);
566 _varyCheck: function(resource)
568 var varyHeader = this.responseHeader(resource, "Vary");
570 varyHeader = varyHeader.replace(/User-Agent/gi, "");
571 varyHeader = varyHeader.replace(/Accept-Encoding/gi, "");
572 varyHeader = varyHeader.replace(/[, ]*/g, "");
574 return varyHeader && varyHeader.length && this.isCacheableResource(resource) && this.freshnessLifetimeGreaterThan(resource, 0);
577 _oneMonthExpirationCheck: function(resource)
579 return this.isCacheableResource(resource) &&
580 !this.hasResponseHeader(resource, "Set-Cookie") &&
581 !this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
582 this.freshnessLifetimeGreaterThan(resource, 0);
585 _oneYearExpirationCheck: function(resource)
587 return this.isCacheableResource(resource) &&
588 !this.hasResponseHeader(resource, "Set-Cookie") &&
589 !this.freshnessLifetimeGreaterThan(resource, 11 * WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
590 this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth);
594 WebInspector.AuditRules.BrowserCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype;
597 WebInspector.AuditRules.ProxyCacheControlRule = function() {
598 WebInspector.AuditRules.CacheControlRule.call(this, "http-proxycache", "Leverage proxy caching");
601 WebInspector.AuditRules.ProxyCacheControlRule.prototype = {
602 runChecks: function(resources, result, callback)
604 this.execCheck("Resources with a \"?\" in the URL are not cached by most proxy caching servers:",
605 this._questionMarkCheck, resources, result);
606 this.execCheck("Consider adding a \"Cache-Control: public\" header to the following resources:",
607 this._publicCachingCheck, resources, result);
608 this.execCheck("The following publicly cacheable resources contain a Set-Cookie header. This security vulnerability can cause cookies to be shared by multiple users.",
609 this._setCookieCacheableCheck, resources, result);
612 _questionMarkCheck: function(resource)
614 return resource.url.indexOf("?") >= 0 && !this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource);
617 _publicCachingCheck: function(resource)
619 return this.isCacheableResource(resource) &&
620 !this.isCompressible(resource) &&
621 !this.responseHeaderMatch(resource, "Cache-Control", "public") &&
622 !this.hasResponseHeader(resource, "Set-Cookie");
625 _setCookieCacheableCheck: function(resource)
627 return this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource);
631 WebInspector.AuditRules.ProxyCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype;
634 WebInspector.AuditRules.ImageDimensionsRule = function()
636 WebInspector.AuditRule.call(this, "page-imagedims", "Specify image dimensions");
639 WebInspector.AuditRules.ImageDimensionsRule.prototype = {
640 doRun: function(resources, result, callback)
642 function doneCallback(context)
644 var map = context.urlToNoDimensionCount;
645 for (var url in map) {
646 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);
647 var value = WebInspector.AuditRuleResult.linkifyDisplayName(url);
649 value += String.sprintf(" (%d uses)", map[url]);
650 entry.addChild(value);
651 result.violationCount++;
653 callback(entry ? result : null);
656 function imageStylesReady(imageId, context, styles)
658 --context.imagesLeft;
660 const node = WebInspector.domAgent.nodeForId(imageId);
661 var src = node.getAttribute("src");
662 if (!src.asParsedURL()) {
663 for (var frameOwnerCandidate = node; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) {
664 if (frameOwnerCandidate.documentURL) {
665 var completeSrc = WebInspector.completeURL(frameOwnerCandidate.documentURL, src);
673 const computedStyle = styles.computedStyle;
674 if (computedStyle.getPropertyValue("position") === "absolute") {
675 if (!context.imagesLeft)
676 doneCallback(context);
680 var widthFound = "width" in styles.styleAttributes;
681 var heightFound = "height" in styles.styleAttributes;
683 for (var i = styles.matchedCSSRules.length - 1; i >= 0 && !(widthFound && heightFound); --i) {
684 var style = styles.matchedCSSRules[i].style;
685 if (style.getPropertyValue("width") !== "")
687 if (style.getPropertyValue("height") !== "")
691 if (!widthFound || !heightFound) {
692 if (src in context.urlToNoDimensionCount)
693 ++context.urlToNoDimensionCount[src];
695 context.urlToNoDimensionCount[src] = 1;
698 if (!context.imagesLeft)
699 doneCallback(context);
702 function receivedImages(imageIds)
704 if (!imageIds || !imageIds.length)
705 return callback(null);
706 var context = {imagesLeft: imageIds.length, urlToNoDimensionCount: {}};
707 for (var i = imageIds.length - 1; i >= 0; --i)
708 WebInspector.cssModel.getStylesAsync(imageIds[i], imageStylesReady.bind(this, imageIds[i], context));
711 function pushImageNodes()
714 var nodes = document.getElementsByTagName("img");
715 for (var i = 0; i < nodes.length; ++i) {
718 var nodeId = this.getNodeId(nodes[i]);
719 nodeIds.push(nodeId);
724 WebInspector.AuditRules.evaluateInTargetWindow(pushImageNodes, null, receivedImages);
728 WebInspector.AuditRules.ImageDimensionsRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
731 WebInspector.AuditRules.CssInHeadRule = function()
733 WebInspector.AuditRule.call(this, "page-cssinhead", "Put CSS in the document head");
736 WebInspector.AuditRules.CssInHeadRule.prototype = {
737 doRun: function(resources, result, callback)
739 function evalCallback(evalResult)
742 return callback(null);
744 var summary = result.addChild("");
746 var outputMessages = [];
747 for (var url in evalResult) {
748 var urlViolations = evalResult[url];
749 if (urlViolations[0]) {
750 result.addChild(String.sprintf("%s style block(s) in the %s body should be moved to the document head.", urlViolations[0], WebInspector.AuditRuleResult.linkifyDisplayName(url)));
751 result.violationCount += urlViolations[0];
753 for (var i = 0; i < urlViolations[1].length; ++i)
754 result.addChild(String.sprintf("Link node %s should be moved to the document head in %s", WebInspector.AuditRuleResult.linkifyDisplayName(urlViolations[1][i]), WebInspector.AuditRuleResult.linkifyDisplayName(url)));
755 result.violationCount += urlViolations[1].length;
757 summary.value = String.sprintf("CSS in the document body adversely impacts rendering performance.");
763 function allViews() {
764 var views = [document.defaultView];
766 while (curView < views.length) {
767 var view = views[curView];
768 var frames = view.frames;
769 for (var i = 0; i < frames.length; ++i) {
770 if (frames[i] !== view)
771 views.push(frames[i]);
778 var views = allViews();
779 var urlToViolationsArray = {};
781 for (var i = 0; i < views.length; ++i) {
786 var inlineStyles = view.document.querySelectorAll("body style");
787 var inlineStylesheets = view.document.querySelectorAll("body link[rel~='stylesheet'][href]");
788 if (!inlineStyles.length && !inlineStylesheets.length)
792 var inlineStylesheetHrefs = [];
793 for (var j = 0; j < inlineStylesheets.length; ++j)
794 inlineStylesheetHrefs.push(inlineStylesheets[j].href);
795 urlToViolationsArray[view.location.href] = [inlineStyles.length, inlineStylesheetHrefs];
797 return found ? urlToViolationsArray : null;
800 WebInspector.AuditRules.evaluateInTargetWindow(routine, null, evalCallback);
804 WebInspector.AuditRules.CssInHeadRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
807 WebInspector.AuditRules.StylesScriptsOrderRule = function()
809 WebInspector.AuditRule.call(this, "page-stylescriptorder", "Optimize the order of styles and scripts");
812 WebInspector.AuditRules.StylesScriptsOrderRule.prototype = {
813 doRun: function(resources, result, callback)
815 function evalCallback(resultValue)
818 return callback(null);
820 var lateCssUrls = resultValue[0];
821 var cssBeforeInlineCount = resultValue[1];
823 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);
824 entry.addURLs(lateCssUrls);
825 result.violationCount += lateCssUrls.length;
827 if (cssBeforeInlineCount) {
828 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"));
829 result.violationCount += cssBeforeInlineCount;
836 var lateStyles = document.querySelectorAll("head script[src] ~ link[rel~='stylesheet'][href]");
837 var cssBeforeInlineCount = document.querySelectorAll("head link[rel~='stylesheet'][href] ~ script:not([src])").length;
838 if (!lateStyles.length && !cssBeforeInlineCount)
841 var lateStyleUrls = [];
842 for (var i = 0; i < lateStyles.length; ++i)
843 lateStyleUrls.push(lateStyles[i].href);
844 return [ lateStyleUrls, cssBeforeInlineCount ];
847 WebInspector.AuditRules.evaluateInTargetWindow(routine, null, evalCallback.bind(this));
851 WebInspector.AuditRules.StylesScriptsOrderRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
854 WebInspector.AuditRules.CookieRuleBase = function(id, name)
856 WebInspector.AuditRule.call(this, id, name);
859 WebInspector.AuditRules.CookieRuleBase.prototype = {
860 doRun: function(resources, result, callback)
863 function resultCallback(receivedCookies, isAdvanced) {
864 self.processCookies(isAdvanced ? receivedCookies : [], resources, result);
867 WebInspector.Cookies.getCookiesAsync(resultCallback);
870 mapResourceCookies: function(resourcesByDomain, allCookies, callback)
872 for (var i = 0; i < allCookies.length; ++i) {
873 for (var resourceDomain in resourcesByDomain) {
874 if (WebInspector.Cookies.cookieDomainMatchesResourceDomain(allCookies[i].domain, resourceDomain))
875 this._callbackForResourceCookiePairs(resourcesByDomain[resourceDomain], allCookies[i], callback);
880 _callbackForResourceCookiePairs: function(resources, cookie, callback)
884 for (var i = 0; i < resources.length; ++i) {
885 if (WebInspector.Cookies.cookieMatchesResourceURL(cookie, resources[i].url))
886 callback(resources[i], cookie);
891 WebInspector.AuditRules.CookieRuleBase.prototype.__proto__ = WebInspector.AuditRule.prototype;
894 WebInspector.AuditRules.CookieSizeRule = function(avgBytesThreshold)
896 WebInspector.AuditRules.CookieRuleBase.call(this, "http-cookiesize", "Minimize cookie size");
897 this._avgBytesThreshold = avgBytesThreshold;
898 this._maxBytesThreshold = 1000;
901 WebInspector.AuditRules.CookieSizeRule.prototype = {
902 _average: function(cookieArray)
905 for (var i = 0; i < cookieArray.length; ++i)
906 total += cookieArray[i].size;
907 return cookieArray.length ? Math.round(total / cookieArray.length) : 0;
910 _max: function(cookieArray)
913 for (var i = 0; i < cookieArray.length; ++i)
914 result = Math.max(cookieArray[i].size, result);
918 processCookies: function(allCookies, resources, result)
920 function maxSizeSorter(a, b)
922 return b.maxCookieSize - a.maxCookieSize;
925 function avgSizeSorter(a, b)
927 return b.avgCookieSize - a.avgCookieSize;
930 var cookiesPerResourceDomain = {};
932 function collectorCallback(resource, cookie)
934 var cookies = cookiesPerResourceDomain[resource.domain];
937 cookiesPerResourceDomain[resource.domain] = cookies;
939 cookies.push(cookie);
942 if (!allCookies.length)
945 var sortedCookieSizes = [];
947 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources,
950 var matchingResourceData = {};
951 this.mapResourceCookies(domainToResourcesMap, allCookies, collectorCallback.bind(this));
953 for (var resourceDomain in cookiesPerResourceDomain) {
954 var cookies = cookiesPerResourceDomain[resourceDomain];
955 sortedCookieSizes.push({
956 domain: resourceDomain,
957 avgCookieSize: this._average(cookies),
958 maxCookieSize: this._max(cookies)
961 var avgAllCookiesSize = this._average(allCookies);
963 var hugeCookieDomains = [];
964 sortedCookieSizes.sort(maxSizeSorter);
966 for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
967 var maxCookieSize = sortedCookieSizes[i].maxCookieSize;
968 if (maxCookieSize > this._maxBytesThreshold)
969 hugeCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(sortedCookieSizes[i].domain) + ": " + Number.bytesToString(maxCookieSize));
972 var bigAvgCookieDomains = [];
973 sortedCookieSizes.sort(avgSizeSorter);
974 for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
975 var domain = sortedCookieSizes[i].domain;
976 var avgCookieSize = sortedCookieSizes[i].avgCookieSize;
977 if (avgCookieSize > this._avgBytesThreshold && avgCookieSize < this._maxBytesThreshold)
978 bigAvgCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(domain) + ": " + Number.bytesToString(avgCookieSize));
980 result.addChild(String.sprintf("The average cookie size for all requests on this page is %s", Number.bytesToString(avgAllCookiesSize)));
983 if (hugeCookieDomains.length) {
984 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);
985 entry.addURLs(hugeCookieDomains);
986 result.violationCount += hugeCookieDomains.length;
989 if (bigAvgCookieDomains.length) {
990 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);
991 entry.addURLs(bigAvgCookieDomains);
992 result.violationCount += bigAvgCookieDomains.length;
997 WebInspector.AuditRules.CookieSizeRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype;
1000 WebInspector.AuditRules.StaticCookielessRule = function(minResources)
1002 WebInspector.AuditRules.CookieRuleBase.call(this, "http-staticcookieless", "Serve static content from a cookieless domain");
1003 this._minResources = minResources;
1006 WebInspector.AuditRules.StaticCookielessRule.prototype = {
1007 processCookies: function(allCookies, resources, result)
1009 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources,
1010 [WebInspector.Resource.Type.Stylesheet,
1011 WebInspector.Resource.Type.Image],
1013 var totalStaticResources = 0;
1014 for (var domain in domainToResourcesMap)
1015 totalStaticResources += domainToResourcesMap[domain].length;
1016 if (totalStaticResources < this._minResources)
1018 var matchingResourceData = {};
1019 this.mapResourceCookies(domainToResourcesMap, allCookies, this._collectorCallback.bind(this, matchingResourceData));
1022 var cookieBytes = 0;
1023 for (var url in matchingResourceData) {
1025 cookieBytes += matchingResourceData[url]
1027 if (badUrls.length < this._minResources)
1030 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);
1031 entry.addURLs(badUrls);
1032 result.violationCount = badUrls.length;
1035 _collectorCallback: function(matchingResourceData, resource, cookie)
1037 matchingResourceData[resource.url] = (matchingResourceData[resource.url] || 0) + cookie.size;
1041 WebInspector.AuditRules.StaticCookielessRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype;