let response = (component, err, superres) => { k-- if (err != null) { if (mostImportantError == null) { mostImportantError = err } else { if (err.status >= 400 && err.status < 600 && err.status < mostImportantError.status) { mostImportantError = err } } } else { data[component.component] = superres.body if (superres.header['cache-control'] != null) { var cache = parseCacheControl(superres.header['cache-control']) if (cache != null) { if (cache['max-age'] != null && typeof cache['max-age'] === 'number' && (header.cache.maxAge == null || header.cache.maxAge > cache['max-age'])) { header.cache.maxAge = cache['max-age'] } if (cache.private === true) { header.cache.private = true } if (cache.public === true) { header.cache.public = true } if (cache['no-cache'] === true) { header.cache.noCache = true } if (cache['no-store'] === true) { header.cache.noStore = true } } } if (superres.header['set-cookie'] != null) { var cookieArray = superres.header['set-cookie'] header.cookie = header.cookie.concat(superres.header['set-cookie']) } } if (k < 1) { if (mostImportantError == null) { var cacheValue = generateCacheHeader(header.cache); if (cacheValue.length > 0) { res.set('Cache-Control', cacheValue) } if (header.cookie.length > 0) { res.set('Set-Cookie', header.cookie) } } cb(mostImportantError, data) } }
return artifacts.requestNetworkRecords(devtoolsLogs).then(records => { const results = []; let queryStringCount = 0; let totalWastedBytes = 0; for (const record of records) { if (!CacheHeaders.isCacheableAsset(record)) continue; /** @type {Map<string, string>} */ const headers = new Map(); for (const header of record._responseHeaders || []) { headers.set(header.name.toLowerCase(), header.value); } const cacheControl = parseCacheControl(headers.get('cache-control')); let cacheLifetimeInSeconds = CacheHeaders.computeCacheLifetimeInSeconds( headers, cacheControl ); // Ignore assets with an explicit no-cache policy if (cacheLifetimeInSeconds === 0) continue; cacheLifetimeInSeconds = cacheLifetimeInSeconds || 0; const cacheHitProbability = CacheHeaders.getCacheHitProbability(cacheLifetimeInSeconds); if (cacheHitProbability > IGNORE_THRESHOLD_IN_PERCENT) continue; const url = URL.elideDataURI(record._url); const totalBytes = record._transferSize || 0; const wastedBytes = (1 - cacheHitProbability) * totalBytes; totalWastedBytes += wastedBytes; if (url.includes('?')) queryStringCount++; results.push({ url, cacheControl, cacheLifetimeMs: cacheLifetimeInSeconds * 1000, cacheHitProbability, totalBytes, wastedBytes, }); } results.sort( (a, b) => a.cacheLifetimeMs - b.cacheLifetimeMs || b.totalBytes - a.totalBytes ); const score = Audit.computeLogNormalScore( totalWastedBytes, context.options.scorePODR, context.options.scoreMedian ); const headings = [ {key: 'url', itemType: 'url', text: 'URL'}, {key: 'cacheLifetimeMs', itemType: 'ms', text: 'Cache TTL', displayUnit: 'duration'}, {key: 'totalBytes', itemType: 'bytes', text: 'Size (KB)', displayUnit: 'kb', granularity: 1}, ]; const summary = {wastedBytes: totalWastedBytes}; const details = Audit.makeTableDetails(headings, results, summary); return { score, rawValue: totalWastedBytes, displayValue: `${results.length} asset${results.length !== 1 ? 's' : ''} found`, extendedInfo: { value: { results, queryStringCount, }, }, details, }; });
return NetworkRecords.request(devtoolsLogs, context).then(records => { const results = []; let queryStringCount = 0; let totalWastedBytes = 0; for (const record of records) { if (!CacheHeaders.isCacheableAsset(record)) continue; /** @type {Map<string, string>} */ const headers = new Map(); for (const header of record.responseHeaders || []) { if (headers.has(header.name.toLowerCase())) { const previousHeaderValue = headers.get(header.name.toLowerCase()); headers.set(header.name.toLowerCase(), `${previousHeaderValue}, ${header.value}`); } else { headers.set(header.name.toLowerCase(), header.value); } } const cacheControl = parseCacheControl(headers.get('cache-control')); if (this.shouldSkipRecord(headers, cacheControl)) { continue; } // Ignore if cacheLifetimeInSeconds is a nonpositive number. let cacheLifetimeInSeconds = CacheHeaders.computeCacheLifetimeInSeconds( headers, cacheControl); if (cacheLifetimeInSeconds !== null && (!Number.isFinite(cacheLifetimeInSeconds) || cacheLifetimeInSeconds <= 0)) { continue; } cacheLifetimeInSeconds = cacheLifetimeInSeconds || 0; // Ignore assets whose cache lifetime is already high enough const cacheHitProbability = CacheHeaders.getCacheHitProbability(cacheLifetimeInSeconds); if (cacheHitProbability > IGNORE_THRESHOLD_IN_PERCENT) continue; const url = URL.elideDataURI(record.url); const totalBytes = record.transferSize || 0; const wastedBytes = (1 - cacheHitProbability) * totalBytes; totalWastedBytes += wastedBytes; if (url.includes('?')) queryStringCount++; // Include cacheControl info (if it exists) per url as a diagnostic. /** @type {LH.Audit.Details.Diagnostic|undefined} */ let diagnostic; if (cacheControl) { diagnostic = { type: 'diagnostic', ...cacheControl, }; } results.push({ url, diagnostic, cacheLifetimeMs: cacheLifetimeInSeconds * 1000, cacheHitProbability, totalBytes, wastedBytes, }); } results.sort((a, b) => { return a.cacheLifetimeMs - b.cacheLifetimeMs || b.totalBytes - a.totalBytes || a.url.localeCompare(b.url); }); const score = Audit.computeLogNormalScore( totalWastedBytes, context.options.scorePODR, context.options.scoreMedian ); /** @type {LH.Audit.Details.Table['headings']} */ const headings = [ {key: 'url', itemType: 'url', text: str_(i18n.UIStrings.columnURL)}, // TODO(i18n): pre-compute localized duration {key: 'cacheLifetimeMs', itemType: 'ms', text: str_(i18n.UIStrings.columnCacheTTL), displayUnit: 'duration'}, {key: 'totalBytes', itemType: 'bytes', text: str_(i18n.UIStrings.columnSize), displayUnit: 'kb', granularity: 1}, ]; const summary = {wastedBytes: totalWastedBytes}; const details = Audit.makeTableDetails(headings, results, summary); return { score, rawValue: totalWastedBytes, displayValue: str_(UIStrings.displayValue, {itemCount: results.length}), extendedInfo: { value: { results, queryStringCount, }, }, details, }; });