Reacenda a chama da paixão do seu casamento

Reacenda a chama da paixão do seu casamento

Reacenda a chama da paixão do seu casamento

Clique abaixo, adquira os segredos para restaurar a intimidade e a alegria na sua relação, superando os desafios que afastaram vocês.

Clique abaixo, adquira os segredos para restaurar a intimidade e a alegria na sua relação, superando os desafios que afastaram vocês.

Clique abaixo, adquira os segredos para restaurar a intimidade e a alegria na sua relação, superando os desafios que afastaram vocês.

60% de desconto por tempo limitado

60% de desconto por tempo limitado

60% de desconto por tempo limitado

(De R$ 97, por apenas 37 à vista)

(De R$ 97, por apenas 37 à vista)

(De R$ 97, por apenas 37 à vista)

Provavelmente você já passou por algum desses problemas: (Ou todos)

Provavelmente você já passou por algum desses problemas: (Ou todos)

Provavelmente você já passou por algum desses problemas: (Ou todos)

Brigas constantes e intolerância por faltar comunicação ou ela ser ruim

Brigas constantes e intolerância por faltar comunicação ou ela ser ruim

Brigas constantes e intolerância por faltar comunicação ou ela ser ruim

Esfriamento da relação e Ausência de intimidade sexual

Esfriamento da relação e Ausência de intimidade sexual

Esfriamento da relação e Ausência de intimidade sexual

Distanciamento, pela dinâmica do tempo, ocupação do trabalho e demandas dos filhos

Distanciamento, pela dinâmica do tempo, ocupação do trabalho e demandas dos filhos

Distanciamento, pela dinâmica do tempo, ocupação do trabalho e demandas dos filhos

Resolva esses problemas agora adquirindo a Masterclass com 60% de desconto

Resolva esses problemas agora adquirindo a Masterclass com 60% de desconto

Resolva esses problemas agora adquirindo a Masterclass com 60% de desconto

Se você está cansada de

Se você está cansada de

Se você está cansada de

Viver um casamento frio, distante e marcado por brigas, eu tenho uma palavra de esperança para você: É POSSÍVEL MUDAR.


E essa mudança passa pelo primeiro passo: conhecer o caminho de um casamento verdadeiramente feliz!


Você não aprendeu a se relacionar. Seu cônjuge muito menos! Chegou a hora de ter acesso ao caminho que vai reacender a chama do seu casamento.


Viver um casamento frio, distante e marcado por brigas, eu tenho uma palavra de esperança para você: É POSSÍVEL MUDAR.


E essa mudança passa pelo primeiro passo: conhecer o caminho de um casamento verdadeiramente feliz!


Você não aprendeu a se relacionar. Seu cônjuge muito menos! Chegou a hora de ter acesso ao caminho que vai reacender a chama do seu casamento.


Viver um casamento frio, distante e marcado por brigas, eu tenho uma palavra de esperança para você: É POSSÍVEL MUDAR.


E essa mudança passa pelo primeiro passo: conhecer o caminho de um casamento verdadeiramente feliz!


Você não aprendeu a se relacionar. Seu cônjuge muito menos! Chegou a hora de ter acesso ao caminho que vai reacender a chama do seu casamento.


(De R$ 97, por apenas 37 à vista)

(De R$ 97, por apenas 37 à vista)

(De R$ 97, por apenas 37 à vista)

Nessa aula, você vai aprender

Nessa aula, você vai aprender

Nessa aula, você vai aprender

Pilares da comunicação

Pilares da comunicação

Pilares da comunicação

 Armadilhas comuns de conflito em que a maioria

dos casais caem

 Armadilhas comuns de conflito em que a maioria

dos casais caem

 Armadilhas comuns de conflito em que a maioria

dos casais caem

 Práticas diárias que podem aprofundar seu casamento

 Práticas diárias que podem aprofundar seu casamento

 Práticas diárias que podem aprofundar seu casamento

O que fazer quando você é o único que parece se

importar em se esforçar

O que fazer quando você é o único que parece se

importar em se esforçar

O que fazer quando você é o único que parece se

importar em se esforçar

(De R$ 97, por apenas 37 à vista)

(De R$ 97, por apenas 37 à vista)

(De R$ 97, por apenas 37 à vista)

Você vai aprender diretamente comigo

Você vai aprender diretamente comigo

Você vai aprender diretamente comigo

Bianca Nobre é casada com Marco, mãe, católica, palestrante, amante dos estudos sobre a pessoa humana e sobre suas relações. Teve uma experiência de intimidade na fé católica em 2012, o que fez com que mergulhasse mais profundamente nesse mistério.


Bianca tem experiência no acompanhamento de casais e demandas de relacionamento e como palestrante formando casais desde o namoro até o casamento.


Bianca Nobre é casada com Marco, mãe, católica, palestrante, amante dos estudos sobre a pessoa humana e sobre suas relações. Teve uma experiência de intimidade na fé católica em 2012, o que fez com que mergulhasse mais profundamente nesse mistério.


Bianca tem experiência no acompanhamento de casais e demandas de relacionamento e como palestrante formando casais desde o namoro até o casamento.


Bianca Nobre é casada com Marco, mãe, católica, palestrante, amante dos estudos sobre a pessoa humana e sobre suas relações. Teve uma experiência de intimidade na fé católica em 2012, o que fez com que mergulhasse mais profundamente nesse mistério.


Bianca tem experiência no acompanhamento de casais e demandas de relacionamento e como palestrante formando casais desde o namoro até o casamento.


Estou te dando esse conhecimento valioso por apenas 37 REAIS

Estou te dando esse conhecimento valioso por apenas 37 REAIS

Estou te dando esse conhecimento valioso por apenas 37 REAIS

60% de desconto por tempo limitado

60% de desconto por tempo limitado

60% de desconto por tempo limitado

(De R$ 97, por apenas 37 à vista)

(De R$ 97, por apenas 37 à vista)

(De R$ 97, por apenas 37 à vista)

Bianca, existe uma garantia?

Bianca, existe uma garantia?

Bianca, existe uma garantia?

Bom, pelo preço que estou pedindo para revelar esse conhecimento valioso, eu nem deveria oferecer uma garantia, mas quem me conhece sabe, que eu sempre faço o impossível para ajudar pessoas a terem um casamento feliz!


7 dias para pedir seu dinheiro de volta – sem perguntas, sem complicação.

Bom, pelo preço que estou pedindo para revelar esse conhecimento valioso, eu nem deveria oferecer uma garantia, mas quem me conhece sabe, que eu sempre faço o impossível para ajudar pessoas a terem um casamento feliz!


7 dias para pedir seu dinheiro de volta – sem perguntas, sem complicação.

Bom, pelo preço que estou pedindo para revelar esse conhecimento valioso, eu nem deveria oferecer uma garantia, mas quem me conhece sabe, que eu sempre faço o impossível para ajudar pessoas a terem um casamento feliz!


7 dias para pedir seu dinheiro de volta – sem perguntas, sem complicação.

Agora é a sua vez!

Agora é a sua vez!

Agora é a sua vez!

Hoje você tem a oportunidade de conhecer o caminho para reaquecer sua relação e voltar a ser feliz no seu casamento, por um preço extremamente barato, mas com uma entrega enorme!

Hoje você tem a oportunidade de conhecer o caminho para reaquecer sua relação e voltar a ser feliz no seu casamento, por um preço extremamente barato, mas com uma entrega enorme!

Hoje você tem a oportunidade de conhecer o caminho para reaquecer sua relação e voltar a ser feliz no seu casamento, por um preço extremamente barato, mas com uma entrega enorme!

Nobre Finanças e marketing em Psicologia

CNPJ 27.954.752/0001-48


Copyright ©

Todos os direitos reservados.

Política de Privacidade | Termos de Uso

Nobre Finanças e marketing em Psicologia

CNPJ 27.954.752/0001-48


Copyright ©

Todos os direitos reservados.

Política de Privacidade | Termos de Uso

Nobre Finanças e marketing em Psicologia

CNPJ 27.954.752/0001-48


Copyright ©

Todos os direitos reservados.

Política de Privacidade | Termos de Uso

/** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Checks to see if images are displayed only outside of the viewport. * Images requested after TTI are not flagged as violations. */ import {ByteEfficiencyAudit} from './byte-efficiency-audit.js'; import {NetworkRequest} from '../../lib/network-request.js'; import {Sentry} from '../../lib/sentry.js'; import UrlUtils from '../../lib/url-utils.js'; import * as i18n from '../../lib/i18n/i18n.js'; import {Interactive} from '../../computed/metrics/interactive.js'; import {ProcessedTrace} from '../../computed/processed-trace.js'; const UIStrings = { /** Imperative title of a Lighthouse audit that tells the user to defer loading offscreen images. Offscreen images are images located outside of the visible browser viewport. As they are unseen by the user and slow down page load, they should be loaded later, closer to when the user is going to see them. This is displayed in a list of audit titles that Lighthouse generates. */ title: 'Defer offscreen images', /** Description of a Lighthouse audit that tells the user *why* they should defer loading offscreen images. Offscreen images are images located outside of the visible browser viewport. As they are unseen by the user and slow down page load, they should be loaded later, closer to when the user is going to see them. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */ description: 'Consider lazy-loading offscreen and hidden images after all critical resources have ' + 'finished loading to lower time to interactive. ' + '[Learn how to defer offscreen images](https://developer.chrome.com/docs/lighthouse/performance/offscreen-images/).', }; const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); // See https://github.com/GoogleChrome/lighthouse/issues/10471 for discussion about the thresholds here. const ALLOWABLE_OFFSCREEN_IN_PX = 100; const ALLOWABLE_OFFSCREEN_BOTTOM_IN_VIEWPORTS = 3; const IGNORE_THRESHOLD_IN_BYTES = 2048; const IGNORE_THRESHOLD_IN_PERCENT = 75; const IGNORE_THRESHOLD_IN_MS = 50; /** @typedef {{node: LH.Audit.Details.NodeValue, url: string, requestStartTime: number, totalBytes: number, wastedBytes: number, wastedPercent: number}} WasteResult */ class OffscreenImages extends ByteEfficiencyAudit { /** * @return {LH.Audit.Meta} */ static get meta() { return { id: 'offscreen-images', title: str_(UIStrings.title), description: str_(UIStrings.description), scoreDisplayMode: ByteEfficiencyAudit.SCORING_MODES.METRIC_SAVINGS, supportedModes: ['navigation'], guidanceLevel: 2, requiredArtifacts: ['ImageElements', 'ViewportDimensions', 'GatherContext', 'devtoolsLogs', 'traces', 'URL'], }; } /** * @param {{top: number, bottom: number, left: number, right: number}} imageRect * @param {{innerWidth: number, innerHeight: number}} viewportDimensions * @return {number} */ static computeVisiblePixels(imageRect, viewportDimensions) { const innerWidth = viewportDimensions.innerWidth; const innerHeight = viewportDimensions.innerHeight; const allowableOffscreenBottomInPx = ALLOWABLE_OFFSCREEN_BOTTOM_IN_VIEWPORTS * viewportDimensions.innerHeight; const top = Math.max(imageRect.top, -1 * ALLOWABLE_OFFSCREEN_IN_PX); const right = Math.min(imageRect.right, innerWidth + ALLOWABLE_OFFSCREEN_IN_PX); const bottom = Math.min(imageRect.bottom, innerHeight + allowableOffscreenBottomInPx); const left = Math.max(imageRect.left, -1 * ALLOWABLE_OFFSCREEN_IN_PX); return Math.max(right - left, 0) * Math.max(bottom - top, 0); } /** * @param {LH.Artifacts.ImageElement} image * @param {{innerWidth: number, innerHeight: number}} viewportDimensions * @param {Array} networkRecords * @return {null|Error|WasteResult} */ static computeWaste(image, viewportDimensions, networkRecords) { const networkRecord = networkRecords.find(record => record.url === image.src); // If we don't know how big it was, we can't really report savings, treat it as passed. if (!networkRecord) return null; // If the image had its loading behavior explicitly controlled already, treat it as passed. if (image.loading === 'lazy' || image.loading === 'eager') return null; const url = UrlUtils.elideDataURI(image.src); const totalPixels = image.displayedWidth * image.displayedHeight; const visiblePixels = this.computeVisiblePixels(image.clientRect, viewportDimensions); // Treat images with 0 area as if they're offscreen. See https://github.com/GoogleChrome/lighthouse/issues/1914 const wastedRatio = totalPixels === 0 ? 1 : 1 - visiblePixels / totalPixels; const totalBytes = NetworkRequest.getResourceSizeOnNetwork(networkRecord); const wastedBytes = Math.round(totalBytes * wastedRatio); if (!Number.isFinite(wastedRatio)) { return new Error(`Invalid image sizing information ${url}`); } return { node: ByteEfficiencyAudit.makeNodeItem(image.node), url, requestStartTime: networkRecord.networkRequestTime, totalBytes, wastedBytes, wastedPercent: 100 * wastedRatio, }; } /** * Filters out image requests that were requested after the last long task based on lantern timings. * * @param {WasteResult[]} images * @param {LH.Artifacts.LanternMetric} lanternMetricData */ static filterLanternResults(images, lanternMetricData) { const nodeTimings = lanternMetricData.pessimisticEstimate.nodeTimings; // Find the last long task start time let lastLongTaskStartTime = 0; // Find the start time of all requests /** @type {Map} */ const startTimesByURL = new Map(); for (const [node, timing] of nodeTimings) { if (node.type === 'cpu' && timing.duration >= 50) { lastLongTaskStartTime = Math.max(lastLongTaskStartTime, timing.startTime); } else if (node.type === 'network') { startTimesByURL.set(node.request.url, timing.startTime); } } return images.filter(image => { // Filter out images that had little waste if (image.wastedBytes < IGNORE_THRESHOLD_IN_BYTES) return false; if (image.wastedPercent < IGNORE_THRESHOLD_IN_PERCENT) return false; // Filter out images that started after the last long task const imageRequestStartTime = startTimesByURL.get(image.url) || 0; return imageRequestStartTime < lastLongTaskStartTime - IGNORE_THRESHOLD_IN_MS; }); } /** * Filters out image requests that were requested after TTI. * * @param {WasteResult[]} images * @param {number} interactiveTimestamp */ static filterObservedResults(images, interactiveTimestamp) { return images.filter(image => { if (image.wastedBytes < IGNORE_THRESHOLD_IN_BYTES) return false; if (image.wastedPercent < IGNORE_THRESHOLD_IN_PERCENT) return false; return image.requestStartTime < interactiveTimestamp / 1000 - IGNORE_THRESHOLD_IN_MS; }); } /** * @param {LH.Artifacts} artifacts * @param {Array} networkRecords * @param {LH.Audit.Context} context * @return {Promise} */ static async audit_(artifacts, networkRecords, context) { const images = artifacts.ImageElements; const viewportDimensions = artifacts.ViewportDimensions; const gatherContext = artifacts.GatherContext; const trace = artifacts.traces[ByteEfficiencyAudit.DEFAULT_PASS]; const devtoolsLog = artifacts.devtoolsLogs[ByteEfficiencyAudit.DEFAULT_PASS]; const URL = artifacts.URL; /** @type {string[]} */ const warnings = []; /** @type {Map} */ const resultsMap = new Map(); for (const image of images) { const processed = OffscreenImages.computeWaste(image, viewportDimensions, networkRecords); if (processed === null) { continue; } if (processed instanceof Error) { warnings.push(processed.message); Sentry.captureException(processed, {tags: {audit: this.meta.id}, level: 'warning'}); continue; } // If an image was used more than once, warn only about its least wasteful usage const existing = resultsMap.get(processed.url); if (!existing || existing.wastedBytes > processed.wastedBytes) { resultsMap.set(processed.url, processed); } } const settings = context.settings; let items; const unfilteredResults = Array.from(resultsMap.values()); // get the interactive time or fallback to getting the end of trace time try { const metricComputationData = {trace, devtoolsLog, gatherContext, settings, URL}; const interactive = await Interactive.request(metricComputationData, context); // use interactive to generate items const lanternInteractive = /** @type {LH.Artifacts.LanternMetric} */ (interactive); // Filter out images that were loaded after all CPU activity items = context.settings.throttlingMethod === 'simulate' ? OffscreenImages.filterLanternResults(unfilteredResults, lanternInteractive) : // @ts-expect-error - .timestamp will exist if throttlingMethod isn't lantern OffscreenImages.filterObservedResults(unfilteredResults, interactive.timestamp); } catch (err) { // if the error is during a Lantern run, end of trace may also be inaccurate, so rethrow if (context.settings.throttlingMethod === 'simulate') { throw err; } // use end of trace as a substitute for finding interactive time items = OffscreenImages.filterObservedResults(unfilteredResults, await ProcessedTrace.request(trace, context).then(tot => tot.timestamps.traceEnd)); } /** @type {LH.Audit.Details.Opportunity['headings']} */ const headings = [ {key: 'node', valueType: 'node', label: ''}, {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)}, {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnResourceSize)}, {key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes)}, ]; return { warnings, items, headings, }; } } export default OffscreenImages; export {UIStrings};/** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import {ByteEfficiencyAudit} from './byte-efficiency-audit.js'; import {UnusedJavascriptSummary} from '../../computed/unused-javascript-summary.js'; import {JSBundles} from '../../computed/js-bundles.js'; import * as i18n from '../../lib/i18n/i18n.js'; import {estimateCompressionRatioForContent} from '../../lib/script-helpers.js'; const UIStrings = { /** Imperative title of a Lighthouse audit that tells the user to reduce JavaScript that is never evaluated during page load. This is displayed in a list of audit titles that Lighthouse generates. */ title: 'Reduce unused JavaScript', /** Description of a Lighthouse audit that tells the user *why* they should reduce JavaScript that is never needed/evaluated by the browser. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */ description: 'Reduce unused JavaScript and defer loading scripts until they are required to ' + 'decrease bytes consumed by network activity. [Learn how to reduce unused JavaScript](https://developer.chrome.com/docs/lighthouse/performance/unused-javascript/).', }; const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); const UNUSED_BYTES_IGNORE_THRESHOLD = 20 * 1024; const UNUSED_BYTES_IGNORE_BUNDLE_SOURCE_THRESHOLD = 512; /** * @param {string[]} strings */ function commonPrefix(strings) { if (!strings.length) { return ''; } const maxWord = strings.reduce((a, b) => a > b ? a : b); let prefix = strings.reduce((a, b) => a > b ? b : a); while (!maxWord.startsWith(prefix)) { prefix = prefix.slice(0, -1); } return prefix; } /** * @param {string} string * @param {string} commonPrefix * @return {string} */ function trimCommonPrefix(string, commonPrefix) { if (!commonPrefix) return string; return string.startsWith(commonPrefix) ? '…' + string.slice(commonPrefix.length) : string; } /** * @typedef WasteData * @property {Uint8Array} unusedByIndex * @property {number} unusedLength * @property {number} contentLength */ class UnusedJavaScript extends ByteEfficiencyAudit { /** * @return {LH.Audit.Meta} */ static get meta() { return { id: 'unused-javascript', title: str_(UIStrings.title), description: str_(UIStrings.description), scoreDisplayMode: ByteEfficiencyAudit.SCORING_MODES.METRIC_SAVINGS, guidanceLevel: 1, requiredArtifacts: ['JsUsage', 'Scripts', 'SourceMaps', 'GatherContext', 'devtoolsLogs', 'traces', 'URL'], }; } /** * @param {LH.Artifacts} artifacts * @param {Array} networkRecords * @param {LH.Audit.Context} context * @return {Promise} */ static async audit_(artifacts, networkRecords, context) { const bundles = await JSBundles.request(artifacts, context); const { unusedThreshold = UNUSED_BYTES_IGNORE_THRESHOLD, bundleSourceUnusedThreshold = UNUSED_BYTES_IGNORE_BUNDLE_SOURCE_THRESHOLD, } = context.options || {}; /** @type {Map} */ const compressionRatioByUrl = new Map(); const items = []; for (const [scriptId, scriptCoverage] of Object.entries(artifacts.JsUsage)) { const script = artifacts.Scripts.find(s => s.scriptId === scriptId); if (!script) continue; // This should never happen. const bundle = bundles.find(b => b.script.scriptId === scriptId); const unusedJsSummary = await UnusedJavascriptSummary.request({scriptId, scriptCoverage, bundle}, context); if (unusedJsSummary.wastedBytes === 0 || unusedJsSummary.totalBytes === 0) continue; const compressionRatio = estimateCompressionRatioForContent( compressionRatioByUrl, script.url, artifacts, networkRecords); /** @type {LH.Audit.ByteEfficiencyItem} */ const item = { url: script.url, totalBytes: Math.round(compressionRatio * unusedJsSummary.totalBytes), wastedBytes: Math.round(compressionRatio * unusedJsSummary.wastedBytes), wastedPercent: unusedJsSummary.wastedPercent, }; if (item.wastedBytes <= unusedThreshold) continue; items.push(item); // If there was an error calculating the bundle sizes, we can't // create any sub-items. if (!bundle || 'errorMessage' in bundle.sizes) continue; const sizes = bundle.sizes; // Augment with bundle data. if (unusedJsSummary.sourcesWastedBytes) { const topUnusedSourceSizes = Object.entries(unusedJsSummary.sourcesWastedBytes) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([source, unused]) => { const total = source === '(unmapped)' ? sizes.unmappedBytes : sizes.files[source]; return { source, unused: Math.round(unused * compressionRatio), total: Math.round(total * compressionRatio), }; }) .filter(d => d.unused >= bundleSourceUnusedThreshold); const commonSourcePrefix = commonPrefix(bundle.map.sourceURLs()); item.subItems = { type: 'subitems', items: topUnusedSourceSizes.map(({source, unused, total}) => { return { source: trimCommonPrefix(source, commonSourcePrefix), sourceBytes: total, sourceWastedBytes: unused, }; }), }; } } return { items, headings: [ /* eslint-disable max-len */ {key: 'url', valueType: 'url', subItemsHeading: {key: 'source', valueType: 'code'}, label: str_(i18n.UIStrings.columnURL)}, {key: 'totalBytes', valueType: 'bytes', subItemsHeading: {key: 'sourceBytes'}, label: str_(i18n.UIStrings.columnTransferSize)}, {key: 'wastedBytes', valueType: 'bytes', subItemsHeading: {key: 'sourceWastedBytes'}, label: str_(i18n.UIStrings.columnWastedBytes)}, /* eslint-enable max-len */ ], }; } } export default UnusedJavaScript; export {UIStrings};/** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import parseCacheControl from 'parse-cache-control'; import {Audit} from '../audit.js'; import {NetworkRequest} from '../../lib/network-request.js'; import UrlUtils from '../../lib/url-utils.js'; import {linearInterpolation} from '../../../shared/statistics.js'; import * as i18n from '../../lib/i18n/i18n.js'; import {NetworkRecords} from '../../computed/network-records.js'; const UIStrings = { /** Title of a diagnostic audit that provides detail on the cache policy applies to the page's static assets. Cache refers to browser disk cache, which keeps old versions of network resources around for future use. This is displayed in a list of audit titles that Lighthouse generates. */ title: 'Uses efficient cache policy on static assets', /** Title of a diagnostic audit that provides details on the any page resources that could have been served with more efficient cache policies. Cache refers to browser disk cache, which keeps old versions of network resources around for future use. This imperative title is shown to users when there is a significant amount of assets served with poor cache policies. */ failureTitle: 'Serve static assets with an efficient cache policy', /** Description of a Lighthouse audit that tells the user *why* they need to adopt a long cache lifetime policy. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */ description: 'A long cache lifetime can speed up repeat visits to your page. ' + '[Learn more about efficient cache policies](https://developer.chrome.com/docs/lighthouse/performance/uses-long-cache-ttl/).', /** [ICU Syntax] Label for the audit identifying network resources with inefficient cache values. Clicking this will expand the audit to show the resources. */ displayValue: `{itemCount, plural, =1 {1 resource found} other {# resources found} }`, }; const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); // Ignore assets that have very high likelihood of cache hit const IGNORE_THRESHOLD_IN_PERCENT = 0.925; class CacheHeaders extends Audit { /** * @return {LH.Audit.Meta} */ static get meta() { return { id: 'uses-long-cache-ttl', title: str_(UIStrings.title), failureTitle: str_(UIStrings.failureTitle), description: str_(UIStrings.description), scoreDisplayMode: Audit.SCORING_MODES.METRIC_SAVINGS, guidanceLevel: 3, requiredArtifacts: ['devtoolsLogs'], }; } /** * @return {LH.Audit.ScoreOptions} */ static get defaultOptions() { return { // 50th and 25th percentiles HTTPArchive -> 50 and 75, with p10 derived from them. // https://bigquery.cloud.google.com/table/httparchive:lighthouse.2018_04_01_mobile?pli=1 // see https://www.desmos.com/calculator/uzsyl2hbcb p10: 28 * 1024, median: 128 * 1024, }; } /** * Computes the percent likelihood that a return visit will be within the cache lifetime, based on * Chrome UMA stats see the note below. * @param {number} maxAgeInSeconds * @return {number} */ static getCacheHitProbability(maxAgeInSeconds) { // This array contains the hand wavy distribution of the age of a resource in hours at the time of // cache hit at 0th, 10th, 20th, 30th, etc percentiles. This is used to compute `wastedMs` since there // are clearly diminishing returns to cache duration i.e. 6 months is not 2x better than 3 months. // Based on UMA stats for HttpCache.StaleEntry.Validated.Age, see https://www.desmos.com/calculator/7v0qh1nzvh // Example: a max-age of 12 hours already covers ~50% of cases, doubling to 24 hours covers ~10% more. const RESOURCE_AGE_IN_HOURS_DECILES = [0, 0.2, 1, 3, 8, 12, 24, 48, 72, 168, 8760, Infinity]; if (RESOURCE_AGE_IN_HOURS_DECILES.length !== 12) { throw new Error('deciles 0-10 and 1 for overflow'); } const maxAgeInHours = maxAgeInSeconds / 3600; const upperDecileIndex = RESOURCE_AGE_IN_HOURS_DECILES.findIndex( decile => decile >= maxAgeInHours ); // Clip the likelihood between 0 and 1 if (upperDecileIndex === RESOURCE_AGE_IN_HOURS_DECILES.length - 1) return 1; if (upperDecileIndex === 0) return 0; // Use the two closest decile points as control points const upperDecileValue = RESOURCE_AGE_IN_HOURS_DECILES[upperDecileIndex]; const lowerDecileValue = RESOURCE_AGE_IN_HOURS_DECILES[upperDecileIndex - 1]; const upperDecile = upperDecileIndex / 10; const lowerDecile = (upperDecileIndex - 1) / 10; // Approximate the real likelihood with linear interpolation return linearInterpolation( lowerDecileValue, lowerDecile, upperDecileValue, upperDecile, maxAgeInHours ); } /** * Return max-age if defined, otherwise expires header if defined, and null if not. * @param {Map} headers * @param {ReturnType} cacheControl * @return {?number} */ static computeCacheLifetimeInSeconds(headers, cacheControl) { if (cacheControl && cacheControl['max-age'] !== undefined) { return cacheControl['max-age']; } const expiresHeaders = headers.get('expires'); if (expiresHeaders) { const expires = new Date(expiresHeaders).getTime(); // Invalid expires values MUST be treated as already expired if (!expires) return 0; return Math.ceil((expires - Date.now()) / 1000); } return null; } /** * Given a network record, returns whether we believe the asset is cacheable, i.e. it was a network * request that satisifed the conditions: * * 1. Has a cacheable status code * 2. Has a resource type that corresponds to static assets (image, script, stylesheet, etc). * * Allowing assets with a query string is debatable, PSI considered them non-cacheable with a similar * caveat. * * TODO: Investigate impact in HTTPArchive, experiment with this policy to see what changes. * * @param {LH.Artifacts.NetworkRequest} record * @return {boolean} */ static isCacheableAsset(record) { const CACHEABLE_STATUS_CODES = new Set([200, 203, 206]); /** @type {Set} */ const STATIC_RESOURCE_TYPES = new Set([ NetworkRequest.TYPES.Font, NetworkRequest.TYPES.Image, NetworkRequest.TYPES.Media, NetworkRequest.TYPES.Script, NetworkRequest.TYPES.Stylesheet, ]); // It's not a request loaded over the network, caching makes no sense if (NetworkRequest.isNonNetworkRequest(record)) return false; return ( CACHEABLE_STATUS_CODES.has(record.statusCode) && STATIC_RESOURCE_TYPES.has(record.resourceType || 'Other') ); } /** * Returns true if headers suggest a record should not be cached for a long time. * @param {Map} headers * @param {ReturnType} cacheControl * @return {boolean} */ static shouldSkipRecord(headers, cacheControl) { // The HTTP/1.0 Pragma header can disable caching if cache-control is not set, see https://tools.ietf.org/html/rfc7234#section-5.4 if (!cacheControl && (headers.get('pragma') || '').includes('no-cache')) { return true; } // Ignore assets where policy implies they should not be cached long periods if (cacheControl && ( cacheControl['must-revalidate'] || cacheControl['no-cache'] || cacheControl['no-store'] || cacheControl['stale-while-revalidate'] || cacheControl['private'])) { return true; } return false; } /** * @param {LH.Artifacts} artifacts * @param {LH.Audit.Context} context * @return {Promise} */ static async audit(artifacts, context) { const devtoolsLogs = artifacts.devtoolsLogs[Audit.DEFAULT_PASS]; const records = await NetworkRecords.request(devtoolsLogs, context); const results = []; let totalWastedBytes = 0; for (const record of records) { if (!CacheHeaders.isCacheableAsset(record)) continue; /** @type {Map} */ 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 = UrlUtils.elideDataURI(record.url); const totalBytes = record.transferSize || 0; const wastedBytes = (1 - cacheHitProbability) * totalBytes; totalWastedBytes += wastedBytes; // Include cacheControl info (if it exists) per url as a diagnostic. /** @type {LH.Audit.Details.DebugData|undefined} */ let debugData; if (cacheControl) { debugData = { type: 'debugdata', ...cacheControl, }; } results.push({ url, debugData, cacheLifetimeMs: cacheLifetimeInSeconds * 1000, cacheHitProbability, totalBytes, wastedBytes, }); } results.sort((a, b) => { return a.cacheLifetimeMs - b.cacheLifetimeMs || b.totalBytes - a.totalBytes || a.url.localeCompare(b.url); }); /** @type {LH.Audit.Details.Table['headings']} */ const headings = [ {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)}, // TODO(i18n): pre-compute localized duration {key: 'cacheLifetimeMs', valueType: 'ms', label: str_(i18n.UIStrings.columnCacheTTL), displayUnit: 'duration'}, {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnTransferSize), displayUnit: 'kb', granularity: 1}, ]; const details = Audit.makeTableDetails(headings, results, {wastedBytes: totalWastedBytes, sortedBy: ['totalBytes'], skipSumming: ['cacheLifetimeMs']}); return { score: results.length ? 0 : 1, numericValue: totalWastedBytes, numericUnit: 'byte', displayValue: str_(UIStrings.displayValue, {itemCount: results.length}), details, }; } } export default CacheHeaders; export {UIStrings};