diff --git a/client/data/site-profiler/metrics-dictionaries.ts b/client/data/site-profiler/metrics-dictionaries.ts index d2e6bc9c8be246..d13fd9b35f88e6 100644 --- a/client/data/site-profiler/metrics-dictionaries.ts +++ b/client/data/site-profiler/metrics-dictionaries.ts @@ -92,6 +92,7 @@ export function getBasicMetricsFromPerfReport( metrics?: any ): BasicMetricsScor ttfb: metrics.ttfb, inp: metrics.inp, tbt: metrics.tbt, + overall: metrics.overall, }; return getBasicMetricsScored( basicMetrics ); } diff --git a/client/data/site-profiler/types.ts b/client/data/site-profiler/types.ts index 0f4a210ab13fd3..b36e943c10b364 100644 --- a/client/data/site-profiler/types.ts +++ b/client/data/site-profiler/types.ts @@ -92,7 +92,7 @@ export interface HostingProviderQueryResponse { hosting_provider: HostingProvider; } -export type Metrics = 'cls' | 'lcp' | 'fcp' | 'ttfb' | 'inp' | 'tbt'; +export type Metrics = 'cls' | 'lcp' | 'fcp' | 'ttfb' | 'inp' | 'tbt' | 'overall'; export type Scores = 'good' | 'needs-improvement' | 'poor'; @@ -145,6 +145,7 @@ export type PerformanceMetricsHistory = { cls?: number[]; inp?: number[]; tbt?: number[]; + overall?: number[]; }; }; diff --git a/client/hosting/performance/components/PerformanceReport.tsx b/client/hosting/performance/components/PerformanceReport.tsx index 69084656c0a884..c45ceb6f736b73 100644 --- a/client/hosting/performance/components/PerformanceReport.tsx +++ b/client/hosting/performance/components/PerformanceReport.tsx @@ -39,6 +39,7 @@ export const PerformanceReport = ( { performanceReport={ performanceReport } url={ url } hash={ hash } + showV2 displayThumbnail={ false } displayNewsletterBanner={ false } displayMigrationBanner={ false } diff --git a/client/hosting/performance/components/circular-performance-score/circular-performance-score.tsx b/client/hosting/performance/components/circular-performance-score/circular-performance-score.tsx new file mode 100644 index 00000000000000..8e3ad864e8cec1 --- /dev/null +++ b/client/hosting/performance/components/circular-performance-score/circular-performance-score.tsx @@ -0,0 +1,35 @@ +import { CircularProgressBar } from '@automattic/components'; +import './style.scss'; + +type CircularPerformanceScoreProps = { + score: number; + size: number; + steps?: number; +}; + +export const CircularPerformanceScore = ( { + score, + size, + steps = 100, +}: CircularPerformanceScoreProps ) => { + const getStatus = ( value: number ) => { + if ( value <= 49 ) { + return 'poor'; + } else if ( value > 49 && value < 90 ) { + return 'needs-improvement'; + } + return 'good'; + }; + + return ( +
+ +
{ score }
+
+ ); +}; diff --git a/client/hosting/performance/components/circular-performance-score/style.scss b/client/hosting/performance/components/circular-performance-score/style.scss new file mode 100644 index 00000000000000..cbc0179edc7323 --- /dev/null +++ b/client/hosting/performance/components/circular-performance-score/style.scss @@ -0,0 +1,38 @@ +@import "@automattic/components/src/styles/typography"; + +.circular-performance-bar { + position: relative; + display: inline-block; + + &.good { + color: #00ba37; + .circular__progress-bar .circular__progress-bar-fill-circle { + stroke: #00ba37; + } + } + + &.needs-improvement { + color: #d67709; + .circular__progress-bar .circular__progress-bar-fill-circle { + stroke: #d67709; + } + } + + &.poor { + color: #d63638; + .circular__progress-bar .circular__progress-bar-fill-circle { + stroke: #d63638; + } + } +} + +.circular-performance-score { + position: absolute; + transform: translate(-50%, -50%); + top: 50%; + left: 50%; + font-family: $font-sf-pro-display; + font-size: $font-size-header-small; + font-weight: 400; +} + diff --git a/client/hosting/performance/site-performance.tsx b/client/hosting/performance/site-performance.tsx index 637f865a28d287..b40f13b4b91d59 100644 --- a/client/hosting/performance/site-performance.tsx +++ b/client/hosting/performance/site-performance.tsx @@ -40,10 +40,12 @@ const usePerformanceReport = ( ) => { const { url = '', hash = '' } = wpcom_performance_url || {}; - const { data: basicMetrics } = useUrlBasicMetricsQuery( url, hash, true ); + const { data: basicMetrics, isError } = useUrlBasicMetricsQuery( url, hash, true ); const { final_url: finalUrl, token } = basicMetrics || {}; - const { data: performanceInsights, isLoading: isLoadingInsights } = - useUrlPerformanceInsightsQuery( url, token ?? hash ); + const { data: performanceInsights, isError: isErrorInsights } = useUrlPerformanceInsightsQuery( + url, + token ?? hash + ); const mobileReport = typeof performanceInsights?.mobile === 'string' ? undefined : performanceInsights?.mobile; @@ -73,7 +75,7 @@ const usePerformanceReport = ( url: finalUrl ?? url, hash: getHashOrToken( hash, token, activeTab === 'mobile' ? mobileLoaded : desktopLoaded ), isLoading: activeTab === 'mobile' ? ! mobileLoaded : ! desktopLoaded, - isError: ! isLoadingInsights && ! basicMetrics, + isError: isError || isErrorInsights, }; }; diff --git a/client/performance-profiler/components/core-web-vitals-display/core-web-vitals-details_v2.tsx b/client/performance-profiler/components/core-web-vitals-display/core-web-vitals-details_v2.tsx index bf438cd2446627..119118d3ec65e6 100644 --- a/client/performance-profiler/components/core-web-vitals-display/core-web-vitals-details_v2.tsx +++ b/client/performance-profiler/components/core-web-vitals-display/core-web-vitals-details_v2.tsx @@ -1,5 +1,6 @@ import { useTranslate } from 'i18n-calypso'; import { Metrics, PerformanceMetricsHistory } from 'calypso/data/site-profiler/types'; +import { CircularPerformanceScore } from 'calypso/hosting/performance/components/circular-performance-score/circular-performance-score'; import { metricsNames, metricsTresholds, @@ -30,7 +31,7 @@ export const CoreWebVitalsDetailsV2: React.FC< CoreWebVitalsDetailsProps > = ( { const { name: displayName } = metricsNames[ activeTab ]; const value = metrics[ activeTab ]; - const { good, needsImprovement } = metricsTresholds[ activeTab ]; + const { good, needsImprovement, bad } = metricsTresholds[ activeTab ]; const formatUnit = ( value: number ) => { if ( [ 'lcp', 'fcp', 'ttfb' ].includes( activeTab ) ) { @@ -85,12 +86,14 @@ export const CoreWebVitalsDetailsV2: React.FC< CoreWebVitalsDetailsProps > = ( { const status = mapThresholdsToStatus( activeTab as Metrics, value ); const statusClass = status === 'needsImprovement' ? 'needs-improvement' : status; + const isPerformanceScoreSelected = activeTab === 'overall'; return (
@@ -106,15 +109,37 @@ export const CoreWebVitalsDetailsV2: React.FC< CoreWebVitalsDetailsProps > = ( { } } > { displayName } +
- { displayValue( activeTab as Metrics, value ) } + { isPerformanceScoreSelected ? ( +
+ +
+ ) : ( + displayValue( activeTab as Metrics, value ) + ) }

{ metricValuations[ activeTab ].explanation }   - - { translate( 'Learn more ↗' ) } - + { isPerformanceScoreSelected ? ( + + { translate( 'See calculator ↗' ) } + + ) : ( + + { translate( 'Learn more ↗' ) } + + ) }

@@ -124,10 +149,15 @@ export const CoreWebVitalsDetailsV2: React.FC< CoreWebVitalsDetailsProps > = ( {
{ translate( 'Excellent' ) }
- { translate( '(0–%(to)s%(unit)s)', { - args: { to: formatUnit( good ), unit: displayUnit() }, - comment: 'Displaying a time range, eg. 0-1s', - } ) } + { isPerformanceScoreSelected + ? translate( '(90–%(to)s)', { + args: { to: formatUnit( good ) }, + comment: 'Displaying a percentage range, eg. 90-100', + } ) + : translate( '(0–%(to)s%(unit)s)', { + args: { to: formatUnit( good ), unit: displayUnit() }, + comment: 'Displaying a time range, eg. 0-1s', + } ) }
@@ -135,14 +165,22 @@ export const CoreWebVitalsDetailsV2: React.FC< CoreWebVitalsDetailsProps > = ( {
{ translate( 'Needs Improvement' ) }
- { translate( '(%(from)s–%(to)s%(unit)s)', { - args: { - from: formatUnit( good ), - to: formatUnit( needsImprovement ), - unit: displayUnit(), - }, - comment: 'Displaying a time range, eg. 2-3s', - } ) } + { isPerformanceScoreSelected + ? translate( '(%(from)s–%(to)s)', { + args: { + from: 50, + to: formatUnit( needsImprovement ), + }, + comment: 'Displaying a percentage range, eg. 50-89', + } ) + : translate( '(%(from)s–%(to)s%(unit)s)', { + args: { + from: formatUnit( good ), + to: formatUnit( needsImprovement ), + unit: displayUnit(), + }, + comment: 'Displaying a time range, eg. 2-3s', + } ) }
@@ -150,13 +188,21 @@ export const CoreWebVitalsDetailsV2: React.FC< CoreWebVitalsDetailsProps > = ( {
{ translate( 'Poor' ) }
- { translate( '(Over %(from)s%(unit)s) ', { - args: { - from: formatUnit( needsImprovement ), - unit: displayUnit(), - }, - comment: 'Displaying a time range, eg. >2s', - } ) } + { isPerformanceScoreSelected + ? translate( '(%(from)s-%(to)s) ', { + args: { + from: 0, + to: formatUnit( bad ), + }, + comment: 'Displaying a percentage range, eg. 0-49', + } ) + : translate( '(Over %(from)s%(unit)s) ', { + args: { + from: formatUnit( needsImprovement ), + unit: displayUnit(), + }, + comment: 'Displaying a time range, eg. >2s', + } ) }
diff --git a/client/performance-profiler/components/core-web-vitals-display/index.tsx b/client/performance-profiler/components/core-web-vitals-display/index.tsx index a0cb9d694e0c0c..575ffa835499e1 100644 --- a/client/performance-profiler/components/core-web-vitals-display/index.tsx +++ b/client/performance-profiler/components/core-web-vitals-display/index.tsx @@ -1,5 +1,6 @@ import { useDesktopBreakpoint } from '@automattic/viewport-react'; -import { useState } from 'react'; +import clsx from 'clsx'; +import { lazy, Suspense, useState } from 'react'; import { Metrics, PerformanceMetricsHistory, @@ -8,30 +9,55 @@ import { import { CoreWebVitalsAccordion } from '../core-web-vitals-accordion'; import { MetricTabBar } from '../metric-tab-bar'; import { CoreWebVitalsDetails } from './core-web-vitals-details'; - +import { CoreWebVitalsDetailsV2 } from './core-web-vitals-details_v2'; import './style.scss'; type CoreWebVitalsDisplayProps = Record< Metrics, number > & { history: PerformanceMetricsHistory; audits: Record< string, PerformanceMetricsItemQueryResponse >; recommendationsRef: React.RefObject< HTMLDivElement > | null; + showV2?: boolean; }; +const MetricTabBarV2 = lazy( () => import( '../metric-tab-bar/metric-tab-bar-v2' ) ); + export const CoreWebVitalsDisplay = ( props: CoreWebVitalsDisplayProps ) => { - const defaultTab = 'fcp'; + const defaultTab = props.showV2 ? 'overall' : 'fcp'; const [ activeTab, setActiveTab ] = useState< Metrics | null >( defaultTab ); const isDesktop = useDesktopBreakpoint(); + const details = props.showV2 ? ( + + ) : ( + + ); + + const metricTabBar = props.showV2 ? ( + + + + ) : ( + + ); + return ( <> { isDesktop && ( -
- - +
+ { metricTabBar } + { details }
) } { ! isDesktop && ( diff --git a/client/performance-profiler/components/core-web-vitals-display/style.scss b/client/performance-profiler/components/core-web-vitals-display/style.scss index 2ebbffe77dfaaa..e4afd1ff958bf3 100644 --- a/client/performance-profiler/components/core-web-vitals-display/style.scss +++ b/client/performance-profiler/components/core-web-vitals-display/style.scss @@ -113,6 +113,13 @@ $blueberry-color: #3858e9; } //v2 +.core-web-vitals-display-v2 { + display: flex; + flex-direction: row; + width: 100%; + gap: 16px; +} + .core-web-vitals-display__metric { font-family: $font-sf-pro-display; font-size: $font-size-header; @@ -127,7 +134,7 @@ $blueberry-color: #3858e9; color: #d67709; } - &.poor { + &.bad { color: #d63638; } } diff --git a/client/performance-profiler/components/dashboard-content/index.tsx b/client/performance-profiler/components/dashboard-content/index.tsx index 831f263eedb0e1..68d0151468e371 100644 --- a/client/performance-profiler/components/dashboard-content/index.tsx +++ b/client/performance-profiler/components/dashboard-content/index.tsx @@ -22,6 +22,7 @@ type PerformanceProfilerDashboardContentProps = { displayNewsletterBanner?: boolean; displayMigrationBanner?: boolean; activeTab?: TabType; + showV2?: boolean; }; export const PerformanceProfilerDashboardContent = ( { @@ -33,6 +34,7 @@ export const PerformanceProfilerDashboardContent = ( { displayNewsletterBanner = true, displayMigrationBanner = true, activeTab = TabType.mobile, + showV2 = false, }: PerformanceProfilerDashboardContentProps ) => { const { overall_score, @@ -53,20 +55,22 @@ export const PerformanceProfilerDashboardContent = ( { return (
-
- - { displayThumbnail && ( - + - ) } -
+ { displayThumbnail && ( + + ) } +
+ ) } & { export const MetricTabBar = ( props: Props ) => { const { activeTab, setActiveTab } = props; - return (
- { Object.entries( metricsNames ).map( ( [ key, { name: displayName } ] ) => { - if ( props[ key as Metrics ] === undefined || props[ key as Metrics ] === null ) { - return null; - } + { Object.entries( metricsNames ) + .filter( ( [ name ] ) => name !== 'overall' ) + .map( ( [ key, { name: displayName } ] ) => { + if ( props[ key as Metrics ] === undefined || props[ key as Metrics ] === null ) { + return null; + } - // Only display TBT if INP is not available - if ( key === 'tbt' && props[ 'inp' ] !== undefined && props[ 'inp' ] !== null ) { - return null; - } + // Only display TBT if INP is not available + if ( key === 'tbt' && props[ 'inp' ] !== undefined && props[ 'inp' ] !== null ) { + return null; + } - const status = mapThresholdsToStatus( key as Metrics, props[ key as Metrics ] ); - const statusClassName = status === 'needsImprovement' ? 'needs-improvement' : status; + const status = mapThresholdsToStatus( key as Metrics, props[ key as Metrics ] ); + const statusClassName = status === 'needsImprovement' ? 'needs-improvement' : status; - return ( -
- - ); - } ) } + + ); + } ) }
); }; diff --git a/client/performance-profiler/components/metric-tab-bar/metric-tab-bar-v2.tsx b/client/performance-profiler/components/metric-tab-bar/metric-tab-bar-v2.tsx index 93fe9cd1aa9132..722af8790fb413 100644 --- a/client/performance-profiler/components/metric-tab-bar/metric-tab-bar-v2.tsx +++ b/client/performance-profiler/components/metric-tab-bar/metric-tab-bar-v2.tsx @@ -1,5 +1,12 @@ +import { clsx } from 'clsx'; import { Metrics } from 'calypso/data/site-profiler/types'; -import { MetricTabBar } from '.'; +import { CircularPerformanceScore } from 'calypso/hosting/performance/components/circular-performance-score/circular-performance-score'; +import { + metricsNames, + mapThresholdsToStatus, + displayValue, +} from 'calypso/performance-profiler/utils/metrics'; +import { StatusIndicator } from '../status-indicator'; import './style_v2.scss'; type Props = Record< Metrics, number > & { @@ -7,6 +14,63 @@ type Props = Record< Metrics, number > & { setActiveTab: ( tab: Metrics ) => void; }; -export const MetricTabBarV2 = ( props: Props ) => { - return ; +const MetricTabBarV2 = ( props: Props ) => { + const { activeTab, setActiveTab } = props; + + return ( +
+ + { Object.entries( metricsNames ).map( ( [ key, { name: displayName } ] ) => { + if ( props[ key as Metrics ] === undefined || props[ key as Metrics ] === null ) { + return null; + } + + // Only display TBT if INP is not available + if ( key === 'tbt' && props[ 'inp' ] !== undefined && props[ 'inp' ] !== null ) { + return null; + } + + if ( key === 'overall' ) { + return null; + } + + const status = mapThresholdsToStatus( key as Metrics, props[ key as Metrics ] ); + const statusClassName = status === 'needsImprovement' ? 'needs-improvement' : status; + + return ( + + ); + } ) } +
+ ); }; + +export default MetricTabBarV2; diff --git a/client/performance-profiler/components/metric-tab-bar/style_v2.scss b/client/performance-profiler/components/metric-tab-bar/style_v2.scss index 5a4eafec5c28f6..89bb09c5239f9f 100644 --- a/client/performance-profiler/components/metric-tab-bar/style_v2.scss +++ b/client/performance-profiler/components/metric-tab-bar/style_v2.scss @@ -7,6 +7,8 @@ $blueberry-color: #3858e9; display: flex; flex-direction: column; justify-content: space-between; + min-width: 250px; + align-self: self-start; button { color: inherit; @@ -24,7 +26,7 @@ $blueberry-color: #3858e9; &:not(.active) { border-bottom: 1px solid var(--studio-gray-5); - &:not(:first-child) { + &:not(:nth-child(-n+2)) { border-top: none; } } @@ -54,7 +56,7 @@ $blueberry-color: #3858e9; border-bottom-left-radius: 6px; } - &:first-child { + &:nth-child(2) { /* stylelint-disable-next-line scales/radii */ border-top-left-radius: 6px; /* stylelint-disable-next-line scales/radii */ @@ -101,3 +103,9 @@ $blueberry-color: #3858e9; color: #d63638; } } + +.metric-tab-bar__performance { + margin-bottom: 16px; + /* stylelint-disable-next-line scales/radii */ + border-radius: 6px; +} diff --git a/client/performance-profiler/utils/metrics.ts b/client/performance-profiler/utils/metrics.ts index 2278078aad25f8..dbd95a1eea8959 100644 --- a/client/performance-profiler/utils/metrics.ts +++ b/client/performance-profiler/utils/metrics.ts @@ -19,6 +19,9 @@ export const metricsNames = { tbt: { name: translate( 'Total Blocking Time' ), }, + overall: { + name: translate( 'Performance Score' ), + }, }; export const metricValuations = { @@ -82,6 +85,16 @@ export const metricValuations = { 'Total Blocking Time measures the total amount of time that a page is blocked from responding to user input, such as mouse clicks, screen taps, or keyboard presses. The best sites have a wait time of less than 200 milliseconds.' ), }, + overall: { + good: translate( 'Your site‘s Performance Score is good' ), + needsImprovement: translate( 'Your site‘s Performance Score is moderate' ), + bad: translate( 'Your site‘s Performance Score needs improvement' ), + heading: translate( 'What is Performance Score?' ), + aka: translate( '(PS)' ), + explanation: translate( + 'The performance score is a combined representation of your site‘s individual speed metrics.' + ), + }, }; // bad values are only needed as a maximum value on the scales @@ -116,11 +129,28 @@ export const metricsTresholds = { needsImprovement: 600, bad: 1000, }, + overall: { + good: 100, + needsImprovement: 89, + bad: 49, + }, +}; + +export const getPerformanceStatus = ( value: number ) => { + if ( value <= 49 ) { + return 'bad'; + } else if ( value > 49 && value < 90 ) { + return 'needsImprovement'; + } + return 'good'; }; export const mapThresholdsToStatus = ( metric: Metrics, value: number ): Valuation => { const { good, needsImprovement } = metricsTresholds[ metric ]; + if ( metric === 'overall' ) { + return getPerformanceStatus( value ); + } if ( value <= good ) { return 'good'; }