diff --git a/lighthouse-core/audits/metrics/largest-contentful-paint.js b/lighthouse-core/audits/metrics/largest-contentful-paint.js index f0264e529e5c..380bc5d0b0e9 100644 --- a/lighthouse-core/audits/metrics/largest-contentful-paint.js +++ b/lighthouse-core/audits/metrics/largest-contentful-paint.js @@ -27,21 +27,39 @@ class LargestContentfulPaint extends Audit { title: str_(i18n.UIStrings.largestContentfulPaintMetric), description: str_(UIStrings.description), scoreDisplayMode: Audit.SCORING_MODES.NUMERIC, - requiredArtifacts: ['traces', 'devtoolsLogs'], + requiredArtifacts: ['traces', 'devtoolsLogs', 'TestedAsMobileDevice'], }; } /** - * @return {LH.Audit.ScoreOptions} + * @return {{mobile: {scoring: LH.Audit.ScoreOptions}, desktop: {scoring: LH.Audit.ScoreOptions}}} */ static get defaultOptions() { return { - // 25th and 13th percentiles HTTPArchive -> median and p10 points. - // https://bigquery.cloud.google.com/table/httparchive:lighthouse.2020_02_01_mobile?pli=1 - // https://web.dev/lcp/#what-is-a-good-lcp-score - // see https://www.desmos.com/calculator/1etesp32kt - p10: 2500, - median: 4000, + mobile: { + // 25th and 13th percentiles HTTPArchive -> median and p10 points. + // https://bigquery.cloud.google.com/table/httparchive:lighthouse.2020_02_01_mobile?pli=1 + // https://web.dev/lcp/#what-is-a-good-lcp-score + // see https://www.desmos.com/calculator/1etesp32kt + scoring: { + p10: 2500, + median: 4000, + }, + }, + desktop: { + // 25th and 5th percentiles HTTPArchive -> median and p10 points. + // SELECT + // APPROX_QUANTILES(lcpValue, 100)[OFFSET(5)] AS p05_lcp, + // APPROX_QUANTILES(lcpValue, 100)[OFFSET(25)] AS p25_lcp + // FROM ( + // SELECT CAST(JSON_EXTRACT_SCALAR(payload, "$['_chromeUserTiming.LargestContentfulPaint']") AS NUMERIC) AS lcpValue + // FROM `httparchive.pages.2020_04_01_desktop` + // ) + scoring: { + p10: 1200, + median: 2400, + }, + }, }; } @@ -56,9 +74,12 @@ class LargestContentfulPaint extends Audit { const metricComputationData = {trace, devtoolsLog, settings: context.settings}; const metricResult = await ComputedLcp.request(metricComputationData, context); + const isDesktop = artifacts.TestedAsMobileDevice === false; + const options = isDesktop ? context.options.desktop : context.options.mobile; + return { score: Audit.computeLogNormalScore( - {p10: context.options.p10, median: context.options.median}, + options.scoring, metricResult.timing ), numericValue: metricResult.timing, diff --git a/lighthouse-core/audits/metrics/total-blocking-time.js b/lighthouse-core/audits/metrics/total-blocking-time.js index 76a02f4ad7fe..57362131a1ae 100644 --- a/lighthouse-core/audits/metrics/total-blocking-time.js +++ b/lighthouse-core/audits/metrics/total-blocking-time.js @@ -27,23 +27,41 @@ class TotalBlockingTime extends Audit { title: str_(i18n.UIStrings.totalBlockingTimeMetric), description: str_(UIStrings.description), scoreDisplayMode: Audit.SCORING_MODES.NUMERIC, - requiredArtifacts: ['traces', 'devtoolsLogs'], + requiredArtifacts: ['traces', 'devtoolsLogs', 'TestedAsMobileDevice'], }; } /** - * @return {LH.Audit.ScoreOptions} + * @return {{mobile: {scoring: LH.Audit.ScoreOptions}, desktop: {scoring: LH.Audit.ScoreOptions}}} */ static get defaultOptions() { return { - // According to a cluster telemetry run over top 10k sites on mobile, 5th percentile was 0ms, - // 25th percentile was 270ms and median was 895ms. These numbers include 404 pages. Picking - // thresholds according to our 25/75-th rule will be quite harsh scoring (a single 350ms task) - // after FCP will yield a score of .5. The following coefficients are semi-arbitrarily picked - // to give 600ms jank a score of .5 and 100ms jank a score of .999. We can tweak these numbers - // in the future. See https://www.desmos.com/calculator/bbsv8fedg5 - median: 600, - p10: 287, + mobile: { + // According to a cluster telemetry run over top 10k sites on mobile, 5th percentile was 0ms, + // 25th percentile was 270ms and median was 895ms. These numbers include 404 pages. Picking + // thresholds according to our 25/75-th rule will be quite harsh scoring (a single 350ms task) + // after FCP will yield a score of .5. The following coefficients are semi-arbitrarily picked + // to give 600ms jank a score of .5 and 100ms jank a score of .999. We can tweak these numbers + // in the future. See https://www.desmos.com/calculator/bbsv8fedg5 + scoring: { + p10: 287, + median: 600, + }, + }, + desktop: { + // Chosen in HTTP Archive desktop results to approximate curve easing described above. + // SELECT + // APPROX_QUANTILES(tbtValue, 100)[OFFSET(40)] AS p40_tbt, + // APPROX_QUANTILES(tbtValue, 100)[OFFSET(60)] AS p60_tbt + // FROM ( + // SELECT CAST(JSON_EXTRACT_SCALAR(payload, '$._TotalBlockingTime') AS NUMERIC) AS tbtValue + // FROM `httparchive.pages.2020_04_01_desktop` + // ) + scoring: { + p10: 150, + median: 350, + }, + }, }; } @@ -65,9 +83,12 @@ class TotalBlockingTime extends Audit { const metricComputationData = {trace, devtoolsLog, settings: context.settings}; const metricResult = await ComputedTBT.request(metricComputationData, context); + const isDesktop = artifacts.TestedAsMobileDevice === false; + const options = isDesktop ? context.options.desktop : context.options.mobile; + return { score: Audit.computeLogNormalScore( - {p10: context.options.p10, median: context.options.median}, + options.scoring, metricResult.timing ), numericValue: metricResult.timing, diff --git a/lighthouse-core/test/audits/metrics/largest-contentful-paint-test.js b/lighthouse-core/test/audits/metrics/largest-contentful-paint-test.js new file mode 100644 index 000000000000..c39e8d7b461a --- /dev/null +++ b/lighthouse-core/test/audits/metrics/largest-contentful-paint-test.js @@ -0,0 +1,46 @@ +/** + * @license Copyright 2020 The Lighthouse Authors. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +'use strict'; + +const LCPAudit = require('../../../audits/metrics/largest-contentful-paint.js'); +const defaultOptions = LCPAudit.defaultOptions; + +const trace = require('../../fixtures/traces/lcp-m78.json'); +const devtoolsLog = require('../../fixtures/traces/lcp-m78.devtools.log.json'); + +function generateArtifacts({trace, devtoolsLog, TestedAsMobileDevice}) { + return { + traces: {[LCPAudit.DEFAULT_PASS]: trace}, + devtoolsLogs: {[LCPAudit.DEFAULT_PASS]: devtoolsLog}, + TestedAsMobileDevice, + }; +} + +function generateContext({throttlingMethod}) { + const settings = {throttlingMethod}; + return {options: defaultOptions, settings, computedCache: new Map()}; +} +/* eslint-env jest */ + +describe('Performance: largest-contentful-paint audit', () => { + it('adjusts scoring based on form factor', async () => { + const artifactsMobile = generateArtifacts({trace, devtoolsLog, TestedAsMobileDevice: true}); + const contextMobile = generateContext({throttlingMethod: 'provided'}); + + const outputMobile = await LCPAudit.audit(artifactsMobile, contextMobile); + expect(outputMobile.numericValue).toBeCloseTo(1121.711, 1); + expect(outputMobile.score).toBe(1); + expect(outputMobile.displayValue).toBeDisplayString('1.1\xa0s'); + + const artifactsDesktop = generateArtifacts({trace, devtoolsLog, TestedAsMobileDevice: false}); + const contextDesktop = generateContext({throttlingMethod: 'provided'}); + + const outputDesktop = await LCPAudit.audit(artifactsDesktop, contextDesktop); + expect(outputDesktop.numericValue).toBeCloseTo(1121.711, 1); + expect(outputDesktop.score).toBe(0.92); + expect(outputDesktop.displayValue).toBeDisplayString('1.1\xa0s'); + }); +}); diff --git a/lighthouse-core/test/audits/metrics/total-blocking-time-test.js b/lighthouse-core/test/audits/metrics/total-blocking-time-test.js index 8aad9f0604d5..1e2718f3a00a 100644 --- a/lighthouse-core/test/audits/metrics/total-blocking-time-test.js +++ b/lighthouse-core/test/audits/metrics/total-blocking-time-test.js @@ -6,27 +6,56 @@ 'use strict'; const TBTAudit = require('../../../audits/metrics/total-blocking-time.js'); -const options = TBTAudit.defaultOptions; +const defaultOptions = TBTAudit.defaultOptions; -const pwaTrace = require('../../fixtures/traces/progressive-app-m60.json'); +const trace = require('../../fixtures/traces/progressive-app-m60.json'); +const devtoolsLog = require('../../fixtures/traces/progressive-app-m60.devtools.log.json'); -function generateArtifactsWithTrace(trace) { +const lcpTrace = require('../../fixtures/traces/lcp-m78.json'); +const lcpDevtoolsLog = require('../../fixtures/traces/lcp-m78.devtools.log.json'); + +function generateArtifacts({trace, devtoolsLog, TestedAsMobileDevice}) { return { traces: {[TBTAudit.DEFAULT_PASS]: trace}, - devtoolsLogs: {[TBTAudit.DEFAULT_PASS]: []}, + devtoolsLogs: {[TBTAudit.DEFAULT_PASS]: devtoolsLog}, + TestedAsMobileDevice, }; } + +function generateContext({throttlingMethod}) { + const settings = {throttlingMethod}; + return {options: defaultOptions, settings, computedCache: new Map()}; +} /* eslint-env jest */ describe('Performance: total-blocking-time audit', () => { it('evaluates Total Blocking Time metric properly', async () => { - const artifacts = generateArtifactsWithTrace(pwaTrace); - const settings = {throttlingMethod: 'provided'}; - const context = {options, settings, computedCache: new Map()}; - const output = await TBTAudit.audit(artifacts, context); + const artifacts = generateArtifacts({trace, devtoolsLog, TestedAsMobileDevice: true}); + const context = generateContext({throttlingMethod: 'provided'}); + const output = await TBTAudit.audit(artifacts, context); expect(output.numericValue).toBeCloseTo(48.3, 1); expect(output.score).toBe(1); expect(output.displayValue).toBeDisplayString('50\xa0ms'); }); + + it('adjusts scoring based on form factor', async () => { + const artifactsMobile = generateArtifacts({trace: lcpTrace, + devtoolsLog: lcpDevtoolsLog, TestedAsMobileDevice: true}); + const contextMobile = generateContext({throttlingMethod: 'provided'}); + + const outputMobile = await TBTAudit.audit(artifactsMobile, contextMobile); + expect(outputMobile.numericValue).toBeCloseTo(333, 1); + expect(outputMobile.score).toBe(0.85); + expect(outputMobile.displayValue).toBeDisplayString('330\xa0ms'); + + const artifactsDesktop = generateArtifacts({trace: lcpTrace, + devtoolsLog: lcpDevtoolsLog, TestedAsMobileDevice: false}); + const contextDesktop = generateContext({throttlingMethod: 'provided'}); + + const outputDesktop = await TBTAudit.audit(artifactsDesktop, contextDesktop); + expect(outputDesktop.numericValue).toBeCloseTo(333, 1); + expect(outputDesktop.score).toBe(0.53); + expect(outputDesktop.displayValue).toBeDisplayString('330\xa0ms'); + }); });