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 (
+
+ );
+};
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 (
@@ -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 (
-