1 /* 2 * Copyright (C) 2010 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 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. 17 * 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. 29 */ 30 31 WebInspector.AuditRules.IPAddressRegexp = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; 32 33 WebInspector.AuditRules.CacheableResponseCodes = 34 { 35 200: true, 36 203: true, 37 206: true, 38 300: true, 39 301: true, 40 410: true, 41 42 304: true // Underlying resource is cacheable 43 } 44 45 WebInspector.AuditRules.getDomainToResourcesMap = function(resources, types, needFullResources) 46 { 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) 51 continue; 52 var parsedURL = resource.url.asParsedURL(); 53 if (!parsedURL) 54 continue; 55 var domain = parsedURL.host; 56 var domainResources = domainToResourcesMap[domain]; 57 if (domainResources === undefined) { 58 domainResources = []; 59 domainToResourcesMap[domain] = domainResources; 60 } 61 domainResources.push(needFullResources ? resource : resource.url); 62 } 63 return domainToResourcesMap; 64 } 65 66 WebInspector.AuditRules.GzipRule = function() 67 { 68 WebInspector.AuditRule.call(this, "network-gzip", "Enable gzip compression"); 69 } 70 71 WebInspector.AuditRules.GzipRule.prototype = { 72 doRun: function(resources, result, callback) 73 { 74 var totalSavings = 0; 75 var compressedSize = 0; 76 var candidateSize = 0; 77 var summary = result.addChild("", true); 78 for (var i = 0, length = resources.length; i < length; ++i) { 79 var resource = resources[i]; 80 if (resource.statusCode === 304) 81 continue; // Do not test 304 Not Modified resources as their contents are always empty. 82 if (this._shouldCompress(resource)) { 83 var size = resource.resourceSize; 84 candidateSize += size; 85 if (this._isCompressed(resource)) { 86 compressedSize += size; 87 continue; 88 } 89 var savings = 2 * size / 3; 90 totalSavings += savings; 91 summary.addChild(String.sprintf("%s could save ~%s", WebInspector.AuditRuleResult.linkifyDisplayName(resource.url), Number.bytesToString(savings))); 92 result.violationCount++; 93 } 94 } 95 if (!totalSavings) 96 return callback(null); 97 summary.value = String.sprintf("Compressing the following resources with gzip could reduce their transfer size by about two thirds (~%s):", Number.bytesToString(totalSavings)); 98 callback(result); 99 }, 100 101 _isCompressed: function(resource) 102 { 103 var encodingHeader = resource.responseHeaders["Content-Encoding"]; 104 if (!encodingHeader) 105 return false; 106 107 return /\b(?:gzip|deflate)\b/.test(encodingHeader); 108 }, 109 110 _shouldCompress: function(resource) 111 { 112 return WebInspector.Resource.Type.isTextType(resource.type) && resource.domain && resource.resourceSize !== undefined && resource.resourceSize > 150; 113 } 114 } 115 116 WebInspector.AuditRules.GzipRule.prototype.__proto__ = WebInspector.AuditRule.prototype; 117 118 119 WebInspector.AuditRules.CombineExternalResourcesRule = function(id, name, type, resourceTypeName, allowedPerDomain) 120 { 121 WebInspector.AuditRule.call(this, id, name); 122 this._type = type; 123 this._resourceTypeName = resourceTypeName; 124 this._allowedPerDomain = allowedPerDomain; 125 } 126 127 WebInspector.AuditRules.CombineExternalResourcesRule.prototype = { 128 doRun: function(resources, result, callback) 129 { 130 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, [this._type]); 131 var penalizedResourceCount = 0; 132 // TODO: refactor according to the chosen i18n approach 133 var summary = result.addChild("", true); 134 for (var domain in domainToResourcesMap) { 135 var domainResources = domainToResourcesMap[domain]; 136 var extraResourceCount = domainResources.length - this._allowedPerDomain; 137 if (extraResourceCount <= 0) 138 continue; 139 penalizedResourceCount += extraResourceCount - 1; 140 summary.addChild(String.sprintf("%d %s resources served from %s.", domainResources.length, this._resourceTypeName, WebInspector.AuditRuleResult.resourceDomain(domain))); 141 result.violationCount += domainResources.length; 142 } 143 if (!penalizedResourceCount) 144 return callback(null); 145 146 summary.value = "There are multiple resources served from same domain. Consider combining them into as few files as possible."; 147 callback(result); 148 } 149 } 150 151 WebInspector.AuditRules.CombineExternalResourcesRule.prototype.__proto__ = WebInspector.AuditRule.prototype; 152 153 154 WebInspector.AuditRules.CombineJsResourcesRule = function(allowedPerDomain) { 155 WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externaljs", "Combine external JavaScript", WebInspector.Resource.Type.Script, "JavaScript", allowedPerDomain); 156 } 157 158 WebInspector.AuditRules.CombineJsResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype; 159 160 161 WebInspector.AuditRules.CombineCssResourcesRule = function(allowedPerDomain) { 162 WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externalcss", "Combine external CSS", WebInspector.Resource.Type.Stylesheet, "CSS", allowedPerDomain); 163 } 164 165 WebInspector.AuditRules.CombineCssResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype; 166 167 168 WebInspector.AuditRules.MinimizeDnsLookupsRule = function(hostCountThreshold) { 169 WebInspector.AuditRule.call(this, "network-minimizelookups", "Minimize DNS lookups"); 170 this._hostCountThreshold = hostCountThreshold; 171 } 172 173 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype = { 174 doRun: function(resources, result, callback) 175 { 176 var summary = result.addChild(""); 177 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, undefined); 178 for (var domain in domainToResourcesMap) { 179 if (domainToResourcesMap[domain].length > 1) 180 continue; 181 var parsedURL = domain.asParsedURL(); 182 if (!parsedURL) 183 continue; 184 if (!parsedURL.host.search(WebInspector.AuditRules.IPAddressRegexp)) 185 continue; // an IP address 186 summary.addSnippet(match[2]); 187 result.violationCount++; 188 } 189 if (!summary.children || summary.children.length <= this._hostCountThreshold) 190 return callback(null); 191 192 summary.value = "The following domains only serve one resource each. If possible, avoid the extra DNS lookups by serving these resources from existing domains."; 193 callback(result); 194 } 195 } 196 197 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype.__proto__ = WebInspector.AuditRule.prototype; 198 199 200 WebInspector.AuditRules.ParallelizeDownloadRule = function(optimalHostnameCount, minRequestThreshold, minBalanceThreshold) 201 { 202 WebInspector.AuditRule.call(this, "network-parallelizehosts", "Parallelize downloads across hostnames"); 203 this._optimalHostnameCount = optimalHostnameCount; 204 this._minRequestThreshold = minRequestThreshold; 205 this._minBalanceThreshold = minBalanceThreshold; 206 } 207 208 209 WebInspector.AuditRules.ParallelizeDownloadRule.prototype = { 210 doRun: function(resources, result, callback) 211 { 212 function hostSorter(a, b) 213 { 214 var aCount = domainToResourcesMap[a].length; 215 var bCount = domainToResourcesMap[b].length; 216 return (aCount < bCount) ? 1 : (aCount == bCount) ? 0 : -1; 217 } 218 219 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap( 220 resources, 221 [WebInspector.Resource.Type.Stylesheet, WebInspector.Resource.Type.Image], 222 true); 223 224 var hosts = []; 225 for (var url in domainToResourcesMap) 226 hosts.push(url); 227 228 if (!hosts.length) 229 return callback(null); // no hosts (local file or something) 230 231 hosts.sort(hostSorter); 232 233 var optimalHostnameCount = this._optimalHostnameCount; 234 if (hosts.length > optimalHostnameCount) 235 hosts.splice(optimalHostnameCount); 236 237 var busiestHostResourceCount = domainToResourcesMap[hosts[0]].length; 238 var resourceCountAboveThreshold = busiestHostResourceCount - this._minRequestThreshold; 239 if (resourceCountAboveThreshold <= 0) 240 return callback(null); 241 242 var avgResourcesPerHost = 0; 243 for (var i = 0, size = hosts.length; i < size; ++i) 244 avgResourcesPerHost += domainToResourcesMap[hosts[i]].length; 245 246 // Assume optimal parallelization. 247 avgResourcesPerHost /= optimalHostnameCount; 248 avgResourcesPerHost = Math.max(avgResourcesPerHost, 1); 249 250 var pctAboveAvg = (resourceCountAboveThreshold / avgResourcesPerHost) - 1.0; 251 var minBalanceThreshold = this._minBalanceThreshold; 252 if (pctAboveAvg < minBalanceThreshold) 253 return callback(null); 254 255 var resourcesOnBusiestHost = domainToResourcesMap[hosts[0]]; 256 var entry = result.addChild(String.sprintf("This page makes %d parallelizable requests to %s. Increase download parallelization by distributing the following requests across multiple hostnames.", busiestHostResourceCount, hosts[0]), true); 257 for (var i = 0; i < resourcesOnBusiestHost.length; ++i) 258 entry.addURL(resourcesOnBusiestHost[i].url); 259 260 result.violationCount = resourcesOnBusiestHost.length; 261 callback(result); 262 } 263 } 264 265 WebInspector.AuditRules.ParallelizeDownloadRule.prototype.__proto__ = WebInspector.AuditRule.prototype; 266 267 268 // The reported CSS rule size is incorrect (parsed != original in WebKit), 269 // so use percentages instead, which gives a better approximation. 270 WebInspector.AuditRules.UnusedCssRule = function() 271 { 272 WebInspector.AuditRule.call(this, "page-unusedcss", "Remove unused CSS rules"); 273 } 274 275 WebInspector.AuditRules.UnusedCssRule.prototype = { 276 doRun: function(resources, result, callback) 277 { 278 var self = this; 279 280 function evalCallback(styleSheets) { 281 if (!styleSheets.length) 282 return callback(null); 283 284 var pseudoSelectorRegexp = /:hover|:link|:active|:visited|:focus|:before|:after/; 285 var selectors = []; 286 var testedSelectors = {}; 287 for (var i = 0; i < styleSheets.length; ++i) { 288 var styleSheet = styleSheets[i]; 289 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) { 290 var selectorText = styleSheet.rules[curRule].selectorText; 291 if (selectorText.match(pseudoSelectorRegexp) || testedSelectors[selectorText]) 292 continue; 293 selectors.push(selectorText); 294 testedSelectors[selectorText] = 1; 295 } 296 } 297 298 function selectorsCallback(callback, styleSheets, testedSelectors, foundSelectors) 299 { 300 var inlineBlockOrdinal = 0; 301 var totalStylesheetSize = 0; 302 var totalUnusedStylesheetSize = 0; 303 var summary; 304 305 for (var i = 0; i < styleSheets.length; ++i) { 306 var styleSheet = styleSheets[i]; 307 var stylesheetSize = 0; 308 var unusedStylesheetSize = 0; 309 var unusedRules = []; 310 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) { 311 var rule = styleSheet.rules[curRule]; 312 // Exact computation whenever source ranges are available. 313 var textLength = (rule.selectorRange && rule.style.range && rule.style.range.end) ? rule.style.range.end - rule.selectorRange.start + 1 : 0; 314 if (!textLength && rule.style.cssText) 315 textLength = rule.style.cssText.length + rule.selectorText.length; 316 stylesheetSize += textLength; 317 if (!testedSelectors[rule.selectorText] || foundSelectors[rule.selectorText]) 318 continue; 319 unusedStylesheetSize += textLength; 320 unusedRules.push(rule.selectorText); 321 } 322 totalStylesheetSize += stylesheetSize; 323 totalUnusedStylesheetSize += unusedStylesheetSize; 324 325 if (!unusedRules.length) 326 continue; 327 328 var resource = WebInspector.resourceForURL(styleSheet.sourceURL); 329 var isInlineBlock = resource && resource.type == WebInspector.Resource.Type.Document; 330 var url = !isInlineBlock ? WebInspector.AuditRuleResult.linkifyDisplayName(styleSheet.sourceURL) : String.sprintf("Inline block #%d", ++inlineBlockOrdinal); 331 var pctUnused = Math.round(100 * unusedStylesheetSize / stylesheetSize); 332 if (!summary) 333 summary = result.addChild("", true); 334 var entry = summary.addChild(String.sprintf("%s: %s (%d%%) is not used by the current page.", url, Number.bytesToString(unusedStylesheetSize), pctUnused)); 335 336 for (var j = 0; j < unusedRules.length; ++j) 337 entry.addSnippet(unusedRules[j]); 338 339 result.violationCount += unusedRules.length; 340 } 341 342 if (!totalUnusedStylesheetSize) 343 return callback(null); 344 345 var totalUnusedPercent = Math.round(100 * totalUnusedStylesheetSize / totalStylesheetSize); 346 summary.value = String.sprintf("%s (%d%%) of CSS is not used by the current page.", Number.bytesToString(totalUnusedStylesheetSize), totalUnusedPercent); 347 348 callback(result); 349 } 350 351 var foundSelectors = {}; 352 function queryCallback(boundSelectorsCallback, selector, styleSheets, testedSelectors, nodeId) 353 { 354 if (nodeId) 355 foundSelectors[selector] = true; 356 if (boundSelectorsCallback) 357 boundSelectorsCallback(foundSelectors); 358 } 359 360 function documentLoaded(selectors, document) { 361 for (var i = 0; i < selectors.length; ++i) 362 WebInspector.domAgent.querySelector(document.id, selectors[i], queryCallback.bind(null, i === selectors.length - 1 ? selectorsCallback.bind(null, callback, styleSheets, testedSelectors) : null, selectors[i], styleSheets, testedSelectors)); 363 } 364 365 WebInspector.domAgent.requestDocument(documentLoaded.bind(null, selectors)); 366 } 367 368 function styleSheetCallback(styleSheets, sourceURL, continuation, styleSheet) 369 { 370 if (styleSheet) { 371 styleSheet.sourceURL = sourceURL; 372 styleSheets.push(styleSheet); 373 } 374 if (continuation) 375 continuation(styleSheets); 376 } 377 378 function allStylesCallback(error, styleSheetInfos) 379 { 380 if (error || !styleSheetInfos || !styleSheetInfos.length) 381 return evalCallback([]); 382 var styleSheets = []; 383 for (var i = 0; i < styleSheetInfos.length; ++i) { 384 var info = styleSheetInfos[i]; 385 WebInspector.CSSStyleSheet.createForId(info.styleSheetId, styleSheetCallback.bind(null, styleSheets, info.sourceURL, i == styleSheetInfos.length - 1 ? evalCallback : null)); 386 } 387 } 388 389 CSSAgent.getAllStyleSheets(allStylesCallback); 390 } 391 } 392 393 WebInspector.AuditRules.UnusedCssRule.prototype.__proto__ = WebInspector.AuditRule.prototype; 394 395 396 WebInspector.AuditRules.CacheControlRule = function(id, name) 397 { 398 WebInspector.AuditRule.call(this, id, name); 399 } 400 401 WebInspector.AuditRules.CacheControlRule.MillisPerMonth = 1000 * 60 * 60 * 24 * 30; 402 403 WebInspector.AuditRules.CacheControlRule.prototype = { 404 405 doRun: function(resources, result, callback) 406 { 407 var cacheableAndNonCacheableResources = this._cacheableAndNonCacheableResources(resources); 408 if (cacheableAndNonCacheableResources[0].length) 409 this.runChecks(cacheableAndNonCacheableResources[0], result); 410 this.handleNonCacheableResources(cacheableAndNonCacheableResources[1], result); 411 412 callback(result); 413 }, 414 415 handleNonCacheableResources: function() 416 { 417 }, 418 419 _cacheableAndNonCacheableResources: function(resources) 420 { 421 var processedResources = [[], []]; 422 for (var i = 0; i < resources.length; ++i) { 423 var resource = resources[i]; 424 if (!this.isCacheableResource(resource)) 425 continue; 426 if (this._isExplicitlyNonCacheable(resource)) 427 processedResources[1].push(resource); 428 else 429 processedResources[0].push(resource); 430 } 431 return processedResources; 432 }, 433 434 execCheck: function(messageText, resourceCheckFunction, resources, result) 435 { 436 var resourceCount = resources.length; 437 var urls = []; 438 for (var i = 0; i < resourceCount; ++i) { 439 if (resourceCheckFunction.call(this, resources[i])) 440 urls.push(resources[i].url); 441 } 442 if (urls.length) { 443 var entry = result.addChild(messageText, true); 444 entry.addURLs(urls); 445 result.violationCount += urls.length; 446 } 447 }, 448 449 freshnessLifetimeGreaterThan: function(resource, timeMs) 450 { 451 var dateHeader = this.responseHeader(resource, "Date"); 452 if (!dateHeader) 453 return false; 454 455 var dateHeaderMs = Date.parse(dateHeader); 456 if (isNaN(dateHeaderMs)) 457 return false; 458 459 var freshnessLifetimeMs; 460 var maxAgeMatch = this.responseHeaderMatch(resource, "Cache-Control", "max-age=(\\d+)"); 461 462 if (maxAgeMatch) 463 freshnessLifetimeMs = (maxAgeMatch[1]) ? 1000 * maxAgeMatch[1] : 0; 464 else { 465 var expiresHeader = this.responseHeader(resource, "Expires"); 466 if (expiresHeader) { 467 var expDate = Date.parse(expiresHeader); 468 if (!isNaN(expDate)) 469 freshnessLifetimeMs = expDate - dateHeaderMs; 470 } 471 } 472 473 return (isNaN(freshnessLifetimeMs)) ? false : freshnessLifetimeMs > timeMs; 474 }, 475 476 responseHeader: function(resource, header) 477 { 478 return resource.responseHeaders[header]; 479 }, 480 481 hasResponseHeader: function(resource, header) 482 { 483 return resource.responseHeaders[header] !== undefined; 484 }, 485 486 isCompressible: function(resource) 487 { 488 return WebInspector.Resource.Type.isTextType(resource.type); 489 }, 490 491 isPubliclyCacheable: function(resource) 492 { 493 if (this._isExplicitlyNonCacheable(resource)) 494 return false; 495 496 if (this.responseHeaderMatch(resource, "Cache-Control", "public")) 497 return true; 498 499 return resource.url.indexOf("?") == -1 && !this.responseHeaderMatch(resource, "Cache-Control", "private"); 500 }, 501 502 responseHeaderMatch: function(resource, header, regexp) 503 { 504 return resource.responseHeaders[header] 505 ? resource.responseHeaders[header].match(new RegExp(regexp, "im")) 506 : undefined; 507 }, 508 509 hasExplicitExpiration: function(resource) 510 { 511 return this.hasResponseHeader(resource, "Date") && 512 (this.hasResponseHeader(resource, "Expires") || this.responseHeaderMatch(resource, "Cache-Control", "max-age")); 513 }, 514 515 _isExplicitlyNonCacheable: function(resource) 516 { 517 var hasExplicitExp = this.hasExplicitExpiration(resource); 518 return this.responseHeaderMatch(resource, "Cache-Control", "(no-cache|no-store|must-revalidate)") || 519 this.responseHeaderMatch(resource, "Pragma", "no-cache") || 520 (hasExplicitExp && !this.freshnessLifetimeGreaterThan(resource, 0)) || 521 (!hasExplicitExp && resource.url && resource.url.indexOf("?") >= 0) || 522 (!hasExplicitExp && !this.isCacheableResource(resource)); 523 }, 524 525 isCacheableResource: function(resource) 526 { 527 return resource.statusCode !== undefined && WebInspector.AuditRules.CacheableResponseCodes[resource.statusCode]; 528 } 529 } 530 531 WebInspector.AuditRules.CacheControlRule.prototype.__proto__ = WebInspector.AuditRule.prototype; 532 533 534 WebInspector.AuditRules.BrowserCacheControlRule = function() 535 { 536 WebInspector.AuditRules.CacheControlRule.call(this, "http-browsercache", "Leverage browser caching"); 537 } 538 539 WebInspector.AuditRules.BrowserCacheControlRule.prototype = { 540 handleNonCacheableResources: function(resources, result) 541 { 542 if (resources.length) { 543 var entry = result.addChild("The following resources are explicitly non-cacheable. Consider making them cacheable if possible:", true); 544 result.violationCount += resources.length; 545 for (var i = 0; i < resources.length; ++i) 546 entry.addURL(resources[i].url); 547 } 548 }, 549 550 runChecks: function(resources, result, callback) 551 { 552 this.execCheck("The following resources are missing a cache expiration. Resources that do not specify an expiration may not be cached by browsers:", 553 this._missingExpirationCheck, resources, result); 554 this.execCheck("The following resources specify a \"Vary\" header that disables caching in most versions of Internet Explorer:", 555 this._varyCheck, resources, result); 556 this.execCheck("The following cacheable resources have a short freshness lifetime:", 557 this._oneMonthExpirationCheck, resources, result); 558 559 // Unable to implement the favicon check due to the WebKit limitations. 560 this.execCheck("To further improve cache hit rate, specify an expiration one year in the future for the following cacheable resources:", 561 this._oneYearExpirationCheck, resources, result); 562 }, 563 564 _missingExpirationCheck: function(resource) 565 { 566 return this.isCacheableResource(resource) && !this.hasResponseHeader(resource, "Set-Cookie") && !this.hasExplicitExpiration(resource); 567 }, 568 569 _varyCheck: function(resource) 570 { 571 var varyHeader = this.responseHeader(resource, "Vary"); 572 if (varyHeader) { 573 varyHeader = varyHeader.replace(/User-Agent/gi, ""); 574 varyHeader = varyHeader.replace(/Accept-Encoding/gi, ""); 575 varyHeader = varyHeader.replace(/[, ]*/g, ""); 576 } 577 return varyHeader && varyHeader.length && this.isCacheableResource(resource) && this.freshnessLifetimeGreaterThan(resource, 0); 578 }, 579 580 _oneMonthExpirationCheck: function(resource) 581 { 582 return this.isCacheableResource(resource) && 583 !this.hasResponseHeader(resource, "Set-Cookie") && 584 !this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth) && 585 this.freshnessLifetimeGreaterThan(resource, 0); 586 }, 587 588 _oneYearExpirationCheck: function(resource) 589 { 590 return this.isCacheableResource(resource) && 591 !this.hasResponseHeader(resource, "Set-Cookie") && 592 !this.freshnessLifetimeGreaterThan(resource, 11 * WebInspector.AuditRules.CacheControlRule.MillisPerMonth) && 593 this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth); 594 } 595 } 596 597 WebInspector.AuditRules.BrowserCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype; 598 599 600 WebInspector.AuditRules.ProxyCacheControlRule = function() { 601 WebInspector.AuditRules.CacheControlRule.call(this, "http-proxycache", "Leverage proxy caching"); 602 } 603 604 WebInspector.AuditRules.ProxyCacheControlRule.prototype = { 605 runChecks: function(resources, result, callback) 606 { 607 this.execCheck("Resources with a \"?\" in the URL are not cached by most proxy caching servers:", 608 this._questionMarkCheck, resources, result); 609 this.execCheck("Consider adding a \"Cache-Control: public\" header to the following resources:", 610 this._publicCachingCheck, resources, result); 611 this.execCheck("The following publicly cacheable resources contain a Set-Cookie header. This security vulnerability can cause cookies to be shared by multiple users.", 612 this._setCookieCacheableCheck, resources, result); 613 }, 614 615 _questionMarkCheck: function(resource) 616 { 617 return resource.url.indexOf("?") >= 0 && !this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource); 618 }, 619 620 _publicCachingCheck: function(resource) 621 { 622 return this.isCacheableResource(resource) && 623 !this.isCompressible(resource) && 624 !this.responseHeaderMatch(resource, "Cache-Control", "public") && 625 !this.hasResponseHeader(resource, "Set-Cookie"); 626 }, 627 628 _setCookieCacheableCheck: function(resource) 629 { 630 return this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource); 631 } 632 } 633 634 WebInspector.AuditRules.ProxyCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype; 635 636 637 WebInspector.AuditRules.ImageDimensionsRule = function() 638 { 639 WebInspector.AuditRule.call(this, "page-imagedims", "Specify image dimensions"); 640 } 641 642 WebInspector.AuditRules.ImageDimensionsRule.prototype = { 643 doRun: function(resources, result, callback) 644 { 645 var urlToNoDimensionCount = {}; 646 647 function doneCallback() 648 { 649 for (var url in urlToNoDimensionCount) { 650 var entry = entry || result.addChild("A width and height should be specified for all images in order to speed up page display. The following image(s) are missing a width and/or height:", true); 651 var value = WebInspector.AuditRuleResult.linkifyDisplayName(url); 652 if (urlToNoDimensionCount[url] > 1) 653 value += String.sprintf(" (%d uses)", urlToNoDimensionCount[url]); 654 entry.addChild(value); 655 result.violationCount++; 656 } 657 callback(entry ? result : null); 658 } 659 660 function imageStylesReady(imageId, lastCall, styles) 661 { 662 const node = WebInspector.domAgent.nodeForId(imageId); 663 var src = node.getAttribute("src"); 664 if (!src.asParsedURL()) { 665 for (var frameOwnerCandidate = node; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) { 666 if (frameOwnerCandidate.documentURL) { 667 var completeSrc = WebInspector.completeURL(frameOwnerCandidate.documentURL, src); 668 break; 669 } 670 } 671 } 672 if (completeSrc) 673 src = completeSrc; 674 675 const computedStyle = styles.computedStyle; 676 if (computedStyle.getPropertyValue("position") === "absolute") { 677 if (lastCall) 678 doneCallback(); 679 return; 680 } 681 682 var widthFound = "width" in styles.styleAttributes; 683 var heightFound = "height" in styles.styleAttributes; 684 685 var inlineStyle = styles.inlineStyle; 686 if (inlineStyle) { 687 if (inlineStyle.getPropertyValue("width") !== "") 688 widthFound = true; 689 if (inlineStyle.getPropertyValue("height") !== "") 690 heightFound = true; 691 } 692 693 for (var i = styles.matchedCSSRules.length - 1; i >= 0 && !(widthFound && heightFound); --i) { 694 var style = styles.matchedCSSRules[i].style; 695 if (style.getPropertyValue("width") !== "") 696 widthFound = true; 697 if (style.getPropertyValue("height") !== "") 698 heightFound = true; 699 } 700 701 if (!widthFound || !heightFound) { 702 if (src in urlToNoDimensionCount) 703 ++urlToNoDimensionCount[src]; 704 else 705 urlToNoDimensionCount[src] = 1; 706 } 707 708 if (lastCall) 709 doneCallback(); 710 } 711 712 function getStyles(nodeIds) 713 { 714 if (!nodeIds) { 715 console.error("Failed to get styles"); 716 return; 717 } 718 for (var i = 0; i < nodeIds.length; ++i) 719 WebInspector.cssModel.getStylesAsync(nodeIds[i], imageStylesReady.bind(this, nodeIds[i], i === nodeIds.length - 1)); 720 } 721 722 function onDocumentAvailable(root) 723 { 724 WebInspector.domAgent.querySelectorAll(root.id, "img[src]", getStyles); 725 } 726 727 WebInspector.domAgent.requestDocument(onDocumentAvailable); 728 } 729 } 730 731 WebInspector.AuditRules.ImageDimensionsRule.prototype.__proto__ = WebInspector.AuditRule.prototype; 732 733 734 WebInspector.AuditRules.CssInHeadRule = function() 735 { 736 WebInspector.AuditRule.call(this, "page-cssinhead", "Put CSS in the document head"); 737 } 738 739 WebInspector.AuditRules.CssInHeadRule.prototype = { 740 doRun: function(resources, result, callback) 741 { 742 function evalCallback(evalResult) 743 { 744 if (!evalResult) 745 return callback(null); 746 747 var summary = result.addChild(""); 748 749 var outputMessages = []; 750 for (var url in evalResult) { 751 var urlViolations = evalResult[url]; 752 if (urlViolations[0]) { 753 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))); 754 result.violationCount += urlViolations[0]; 755 } 756 for (var i = 0; i < urlViolations[1].length; ++i) 757 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))); 758 result.violationCount += urlViolations[1].length; 759 } 760 summary.value = String.sprintf("CSS in the document body adversely impacts rendering performance."); 761 callback(result); 762 } 763 764 function externalStylesheetsReceived(root, inlineStyleNodeIds, nodeIds) 765 { 766 if (!nodeIds) { 767 callback(null); 768 return; 769 } 770 771 var externalStylesheetNodeIds = nodeIds; 772 var result = null; 773 if (inlineStyleNodeIds.length || externalStylesheetNodeIds.length) { 774 var urlToViolationsArray = {}; 775 var externalStylesheetHrefs = []; 776 for (var j = 0; j < externalStylesheetNodeIds.length; ++j) { 777 var linkNode = WebInspector.domAgent.nodeForId(externalStylesheetNodeIds[j]); 778 var completeHref = WebInspector.completeURL(linkNode.ownerDocument.documentURL, linkNode.getAttribute("href")); 779 externalStylesheetHrefs.push(completeHref || "<empty>"); 780 } 781 urlToViolationsArray[root.documentURL] = [inlineStyleNodeIds.length, externalStylesheetHrefs]; 782 result = urlToViolationsArray; 783 } 784 evalCallback(result); 785 } 786 787 function inlineStylesReceived(root, nodeIds) 788 { 789 if (!nodeIds) { 790 callback(null); 791 return; 792 } 793 794 WebInspector.domAgent.querySelectorAll(root.id, "body link[rel~='stylesheet'][href]", externalStylesheetsReceived.bind(null, root, nodeIds)); 795 } 796 797 function onDocumentAvailable(root) 798 { 799 WebInspector.domAgent.querySelectorAll(root.id, "body style", inlineStylesReceived.bind(null, root)); 800 } 801 802 WebInspector.domAgent.requestDocument(onDocumentAvailable); 803 } 804 } 805 806 WebInspector.AuditRules.CssInHeadRule.prototype.__proto__ = WebInspector.AuditRule.prototype; 807 808 809 WebInspector.AuditRules.StylesScriptsOrderRule = function() 810 { 811 WebInspector.AuditRule.call(this, "page-stylescriptorder", "Optimize the order of styles and scripts"); 812 } 813 814 WebInspector.AuditRules.StylesScriptsOrderRule.prototype = { 815 doRun: function(resources, result, callback) 816 { 817 function evalCallback(resultValue) 818 { 819 if (!resultValue) 820 return callback(null); 821 822 var lateCssUrls = resultValue[0]; 823 var cssBeforeInlineCount = resultValue[1]; 824 825 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); 826 entry.addURLs(lateCssUrls); 827 result.violationCount += lateCssUrls.length; 828 829 if (cssBeforeInlineCount) { 830 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")); 831 result.violationCount += cssBeforeInlineCount; 832 } 833 callback(result); 834 } 835 836 function cssBeforeInlineReceived(lateStyleIds, nodeIds) 837 { 838 if (!nodeIds) { 839 callback(null); 840 return; 841 } 842 843 var cssBeforeInlineCount = nodeIds.length; 844 var result = null; 845 if (lateStyleIds.length || cssBeforeInlineCount) { 846 var lateStyleUrls = []; 847 for (var i = 0; i < lateStyleIds.length; ++i) { 848 var lateStyleNode = WebInspector.domAgent.nodeForId(lateStyleIds[i]); 849 var completeHref = WebInspector.completeURL(lateStyleNode.ownerDocument.documentURL, lateStyleNode.getAttribute("href")); 850 lateStyleUrls.push(completeHref || "<empty>"); 851 } 852 result = [ lateStyleUrls, cssBeforeInlineCount ]; 853 } 854 855 evalCallback(result); 856 } 857 858 function lateStylesReceived(root, nodeIds) 859 { 860 if (!nodeIds) { 861 callback(null); 862 return; 863 } 864 865 WebInspector.domAgent.querySelectorAll(root.id, "head link[rel~='stylesheet'][href] ~ script:not([src])", cssBeforeInlineReceived.bind(null, nodeIds)); 866 } 867 868 function onDocumentAvailable(root) 869 { 870 WebInspector.domAgent.querySelectorAll(root.id, "head script[src] ~ link[rel~='stylesheet'][href]", lateStylesReceived.bind(null, root)); 871 } 872 873 WebInspector.domAgent.requestDocument(onDocumentAvailable); 874 } 875 } 876 877 WebInspector.AuditRules.StylesScriptsOrderRule.prototype.__proto__ = WebInspector.AuditRule.prototype; 878 879 880 WebInspector.AuditRules.CookieRuleBase = function(id, name) 881 { 882 WebInspector.AuditRule.call(this, id, name); 883 } 884 885 WebInspector.AuditRules.CookieRuleBase.prototype = { 886 doRun: function(resources, result, callback) 887 { 888 var self = this; 889 function resultCallback(receivedCookies, isAdvanced) { 890 self.processCookies(isAdvanced ? receivedCookies : [], resources, result); 891 callback(result); 892 } 893 WebInspector.Cookies.getCookiesAsync(resultCallback); 894 }, 895 896 mapResourceCookies: function(resourcesByDomain, allCookies, callback) 897 { 898 for (var i = 0; i < allCookies.length; ++i) { 899 for (var resourceDomain in resourcesByDomain) { 900 if (WebInspector.Cookies.cookieDomainMatchesResourceDomain(allCookies[i].domain, resourceDomain)) 901 this._callbackForResourceCookiePairs(resourcesByDomain[resourceDomain], allCookies[i], callback); 902 } 903 } 904 }, 905 906 _callbackForResourceCookiePairs: function(resources, cookie, callback) 907 { 908 if (!resources) 909 return; 910 for (var i = 0; i < resources.length; ++i) { 911 if (WebInspector.Cookies.cookieMatchesResourceURL(cookie, resources[i].url)) 912 callback(resources[i], cookie); 913 } 914 } 915 } 916 917 WebInspector.AuditRules.CookieRuleBase.prototype.__proto__ = WebInspector.AuditRule.prototype; 918 919 920 WebInspector.AuditRules.CookieSizeRule = function(avgBytesThreshold) 921 { 922 WebInspector.AuditRules.CookieRuleBase.call(this, "http-cookiesize", "Minimize cookie size"); 923 this._avgBytesThreshold = avgBytesThreshold; 924 this._maxBytesThreshold = 1000; 925 } 926 927 WebInspector.AuditRules.CookieSizeRule.prototype = { 928 _average: function(cookieArray) 929 { 930 var total = 0; 931 for (var i = 0; i < cookieArray.length; ++i) 932 total += cookieArray[i].size; 933 return cookieArray.length ? Math.round(total / cookieArray.length) : 0; 934 }, 935 936 _max: function(cookieArray) 937 { 938 var result = 0; 939 for (var i = 0; i < cookieArray.length; ++i) 940 result = Math.max(cookieArray[i].size, result); 941 return result; 942 }, 943 944 processCookies: function(allCookies, resources, result) 945 { 946 function maxSizeSorter(a, b) 947 { 948 return b.maxCookieSize - a.maxCookieSize; 949 } 950 951 function avgSizeSorter(a, b) 952 { 953 return b.avgCookieSize - a.avgCookieSize; 954 } 955 956 var cookiesPerResourceDomain = {}; 957 958 function collectorCallback(resource, cookie) 959 { 960 var cookies = cookiesPerResourceDomain[resource.domain]; 961 if (!cookies) { 962 cookies = []; 963 cookiesPerResourceDomain[resource.domain] = cookies; 964 } 965 cookies.push(cookie); 966 } 967 968 if (!allCookies.length) 969 return; 970 971 var sortedCookieSizes = []; 972 973 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, 974 null, 975 true); 976 var matchingResourceData = {}; 977 this.mapResourceCookies(domainToResourcesMap, allCookies, collectorCallback.bind(this)); 978 979 for (var resourceDomain in cookiesPerResourceDomain) { 980 var cookies = cookiesPerResourceDomain[resourceDomain]; 981 sortedCookieSizes.push({ 982 domain: resourceDomain, 983 avgCookieSize: this._average(cookies), 984 maxCookieSize: this._max(cookies) 985 }); 986 } 987 var avgAllCookiesSize = this._average(allCookies); 988 989 var hugeCookieDomains = []; 990 sortedCookieSizes.sort(maxSizeSorter); 991 992 for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) { 993 var maxCookieSize = sortedCookieSizes[i].maxCookieSize; 994 if (maxCookieSize > this._maxBytesThreshold) 995 hugeCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(sortedCookieSizes[i].domain) + ": " + Number.bytesToString(maxCookieSize)); 996 } 997 998 var bigAvgCookieDomains = []; 999 sortedCookieSizes.sort(avgSizeSorter); 1000 for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) { 1001 var domain = sortedCookieSizes[i].domain; 1002 var avgCookieSize = sortedCookieSizes[i].avgCookieSize; 1003 if (avgCookieSize > this._avgBytesThreshold && avgCookieSize < this._maxBytesThreshold) 1004 bigAvgCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(domain) + ": " + Number.bytesToString(avgCookieSize)); 1005 } 1006 result.addChild(String.sprintf("The average cookie size for all requests on this page is %s", Number.bytesToString(avgAllCookiesSize))); 1007 1008 var message; 1009 if (hugeCookieDomains.length) { 1010 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); 1011 entry.addURLs(hugeCookieDomains); 1012 result.violationCount += hugeCookieDomains.length; 1013 } 1014 1015 if (bigAvgCookieDomains.length) { 1016 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); 1017 entry.addURLs(bigAvgCookieDomains); 1018 result.violationCount += bigAvgCookieDomains.length; 1019 } 1020 } 1021 } 1022 1023 WebInspector.AuditRules.CookieSizeRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype; 1024 1025 1026 WebInspector.AuditRules.StaticCookielessRule = function(minResources) 1027 { 1028 WebInspector.AuditRules.CookieRuleBase.call(this, "http-staticcookieless", "Serve static content from a cookieless domain"); 1029 this._minResources = minResources; 1030 } 1031 1032 WebInspector.AuditRules.StaticCookielessRule.prototype = { 1033 processCookies: function(allCookies, resources, result) 1034 { 1035 var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, 1036 [WebInspector.Resource.Type.Stylesheet, 1037 WebInspector.Resource.Type.Image], 1038 true); 1039 var totalStaticResources = 0; 1040 for (var domain in domainToResourcesMap) 1041 totalStaticResources += domainToResourcesMap[domain].length; 1042 if (totalStaticResources < this._minResources) 1043 return; 1044 var matchingResourceData = {}; 1045 this.mapResourceCookies(domainToResourcesMap, allCookies, this._collectorCallback.bind(this, matchingResourceData)); 1046 1047 var badUrls = []; 1048 var cookieBytes = 0; 1049 for (var url in matchingResourceData) { 1050 badUrls.push(url); 1051 cookieBytes += matchingResourceData[url] 1052 } 1053 if (badUrls.length < this._minResources) 1054 return; 1055 1056 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); 1057 entry.addURLs(badUrls); 1058 result.violationCount = badUrls.length; 1059 }, 1060 1061 _collectorCallback: function(matchingResourceData, resource, cookie) 1062 { 1063 matchingResourceData[resource.url] = (matchingResourceData[resource.url] || 0) + cookie.size; 1064 } 1065 } 1066 1067 WebInspector.AuditRules.StaticCookielessRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype; 1068