Skip to content
108 changes: 105 additions & 3 deletions config/techreport.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,21 @@
"LCP",
"FCP",
"INP",
"TTFB"
"TTFB",
"overall_sn",
"LCP_sn",
"CLS_sn",
"INP_sn"
],
"cwv_thresholds": {
"LCP": [{ "value": 2500, "label": "Good" }, { "value": 4000, "label": "Needs improvement" }],
"LCP_sn": [{ "value": 2500, "label": "Good" }, { "value": 4000, "label": "Needs improvement" }],
"FCP": [{ "value": 1800, "label": "Good" }, { "value": 3000, "label": "Needs improvement" }],
"TTFB": [{ "value": 800, "label": "Good" }, { "value": 1800, "label": "Needs improvement" }],
"INP": [{ "value": 200, "label": "Good" }, { "value": 500, "label": "Needs improvement" }],
"CLS": [{ "value": 0.1, "label": "Good" }, { "value": 0.25, "label": "Needs improvement" }]
"INP_sn": [{ "value": 200, "label": "Good" }, { "value": 500, "label": "Needs improvement" }],
"CLS": [{ "value": 0.1, "label": "Good" }, { "value": 0.25, "label": "Needs improvement" }],
"CLS_sn": [{ "value": 0.1, "label": "Good" }, { "value": 0.25, "label": "Needs improvement" }]
},
"lighthouse_subcategories": [
"accessibility",
Expand Down Expand Up @@ -324,6 +331,22 @@
{
"label": "Good TTFB",
"value": "TTFB"
},
{
"label": "Good Core Web Vitals (with soft navs)",
"value": "overall_sn"
},
{
"label": "Good LCP (with soft navs)",
"value": "LCP_sn"
},
{
"label": "Good INP (with soft navs)",
"value": "INP_sn"
},
{
"label": "Good CLS (with soft navs)",
"value": "CLS_sn"
}
]
},
Expand Down Expand Up @@ -739,7 +762,11 @@
"INP": "Good INP",
"CLS": "Good CLS",
"FCP": "Good FCP",
"TTFB": "Good TTFB"
"TTFB": "Good TTFB",
"overall_sn": "Good Core Web Vitals (with soft navs)",
"LCP_sn": "Good LCP (with soft navs)",
"INP_sn": "Good INP (with soft navs)",
"CLS_sn": "Good CLS (with soft navs)"
}
},
"cwv_distribution": {
Expand All @@ -755,6 +782,15 @@
"axis_label": "LCP (ms)",
"step": 100
},
"LCP_sn": {
"label": "LCP_sn",
"display_name": "LCP (soft navs)",
Comment thread
max-ostapenko marked this conversation as resolved.
Outdated
"bucketField": "loading_bucket",
"originsField": "lcp_sn_origins",
"unit": "ms",
"axis_label": "LCP with soft navs (ms)",
"step": 100
},
"FCP": {
"label": "FCP",
"bucketField": "loading_bucket",
Expand All @@ -778,13 +814,31 @@
"axis_label": "INP (ms)",
"step": 25
},
"INP_sn": {
"label": "INP_sn",
"display_name": "INP (soft navs)",
"bucketField": "inp_bucket",
"originsField": "inp_sn_origins",
"unit": "ms",
"axis_label": "INP with soft navs (ms)",
"step": 25
},
"CLS": {
"label": "CLS",
"bucketField": "cls_bucket",
"originsField": "cls_origins",
"unit": "",
"axis_label": "CLS",
"step": 0.05
},
"CLS_sn": {
"label": "CLS_sn",
"display_name": "CLS (soft navs)",
"bucketField": "cls_bucket",
"originsField": "cls_sn_origins",
"unit": "",
"axis_label": "CLS with soft navs",
"step": 0.05
}
},
"zone_colors": {
Expand Down Expand Up @@ -938,6 +992,22 @@
{
"label": "Good TTFB",
"value": "TTFB"
},
{
"label": "Good Core Web Vitals (with soft navs)",
"value": "overall_sn"
},
{
"label": "Good LCP (with soft navs)",
"value": "LCP_sn"
},
{
"label": "Good INP (with soft navs)",
"value": "INP_sn"
},
{
"label": "Good CLS (with soft navs)",
"value": "CLS_sn"
}
]
},
Expand Down Expand Up @@ -1440,6 +1510,26 @@
"label": "Good TTFB",
"title": "Good Time To First Byte",
"description": "TTFB is a metric that measures the time between the request for a resource and when the first byte of a response begins to arrive. A good TTFB is less than or equal to 800ms."
},
"overall_sn": {
"label": "Good Core Web Vitals (with soft navs)",
"title": "Good Core Web Vitals (with soft navs)",
"description": "The percentage of origins passing all three Core Web Vitals (LCP, INP, CLS) with a good experience, accounting for soft navigations in single-page applications. Note that if an origin is missing INP data, it's assessed based on the performance of the remaining metrics."
},
"LCP_sn": {
"label": "Good LCP (with soft navs)",
"title": "Good Largest Contentful Paint (with soft navs)",
"description": "Largest Contentful Paint including soft navigations measures perceived load speed across both hard page loads and subsequent in-app soft navigations. Good experiences are less than or equal to 2.5 seconds."
},
"INP_sn": {
"label": "Good INP (with soft navs)",
"title": "Good Interaction to Next Paint (with soft navs)",
"description": "Interaction to Next Paint including soft navigations assesses responsiveness across user interactions within the lifespan of a visit, including interactions occurring during soft navigations. A good experience is less than or equal to 200ms."
},
"CLS_sn": {
"label": "Good CLS (with soft navs)",
"title": "Good Cumulative Layout Shift (with soft navs)",
"description": "Cumulative Layout Shift including soft navigations measures visual stability across both initial loads and subsequent soft navigation page changes. Sites should strive to have a CLS of 0.1 or less for at least 75% of page visits."
}
},
"pageWeight": {
Expand Down Expand Up @@ -1480,20 +1570,32 @@
"origins_eligible_for_fcp": "Eligible",
"origins_eligible_for_inp": "Eligible",
"origins_eligible_for_ttfb": "Eligible",
"origins_eligible_for_cwv_sn": "Eligible",
"origins_eligible_for_cls_sn": "Eligible",
"origins_eligible_for_lcp_sn": "Eligible",
"origins_eligible_for_inp_sn": "Eligible",

"origins_with_good_cwv": "Having good Core Web Vitals",
"origins_with_good_cls": "Having good CLS",
"origins_with_good_lcp": "Having good LCP",
"origins_with_good_fcp": "Having good FCP",
"origins_with_good_inp": "Having good INP",
"origins_with_good_ttfb": "Having good TTFB",
"origins_with_good_cwv_sn": "Having good Core Web Vitals (with soft navs)",
"origins_with_good_cls_sn": "Having good CLS (with soft navs)",
"origins_with_good_lcp_sn": "Having good LCP (with soft navs)",
"origins_with_good_inp_sn": "Having good INP (with soft navs)",

"pct_good_cwv": "% Good Core Web Vitals",
"pct_good_cls": "% Good CLS",
"pct_good_lcp": "% Good LCP",
"pct_good_fcp": "% Good FCP",
"pct_good_inp": "% Good INP",
"pct_good_ttfb": "% Good TTFB",
"pct_good_cwv_sn": "% Good Core Web Vitals (with soft navs)",
"pct_good_cls_sn": "% Good CLS (with soft navs)",
"pct_good_lcp_sn": "% Good LCP (with soft navs)",
"pct_good_inp_sn": "% Good INP (with soft navs)",

"median_lighthouse_score_accessibility": "Median accessibility",
"median_lighthouse_score_performance": "Median performance",
Expand Down
8 changes: 6 additions & 2 deletions src/js/techreport/cwvDistribution.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,17 @@ class CwvDistribution {

// Map the shared metric value (which may be 'overall') to a metric this chart can show
resolveMetric(value) {
if (value && this.pageConfig.cwv_distribution.metric_config[value]) return this.pageConfig.cwv_distribution.metric_config[value].label;
if (value && this.pageConfig?.cwv_distribution?.metric_config?.[value]) return value;
if (value === 'overall_sn') return 'LCP_sn';
Comment thread
max-ostapenko marked this conversation as resolved.
Outdated
return 'LCP';
}

updateTitle() {
const slot = this.root?.querySelector('[data-slot="cwv-distribution-title-metric"]');
if (slot) slot.textContent = this.selectedMetric;
if (slot) {
const cfg = this.pageConfig?.cwv_distribution?.metric_config?.[this.selectedMetric];
slot.textContent = cfg?.display_name || cfg?.label || this.selectedMetric;
Comment thread
max-ostapenko marked this conversation as resolved.
Outdated
}
}

bindEventListeners() {
Expand Down
25 changes: 20 additions & 5 deletions src/js/techreport/geoBreakdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ class GeoBreakdown {
updateTitle() {
const slot = document.querySelector('[data-slot="geo-title-metric"]');
if (!slot) return;
slot.textContent = this.selectedMetric === 'overall' ? 'Core Web Vitals' : this.selectedMetric;
const label = this.pageConfig?.geo_breakdown?.metric_labels?.[this.selectedMetric];
slot.textContent = label ? label.replace(/^Good\s+/, '') : (this.selectedMetric === 'overall' ? 'Core Web Vitals' : this.selectedMetric);
Comment thread
max-ostapenko marked this conversation as resolved.
Outdated
}

bindEventListeners() {
Expand Down Expand Up @@ -137,16 +138,18 @@ class GeoBreakdown {

// Determine bar label and which extra columns to show
const metric = this.selectedMetric;
const barLabel = metric === 'overall' ? 'Good Core Web Vitals' : `Good ${metric}`;
const barLabel = this.pageConfig?.geo_breakdown?.metric_labels?.[metric] || (metric === 'overall' ? 'Good Core Web Vitals' : `Good ${metric}`);
Comment thread
max-ostapenko marked this conversation as resolved.
Outdated
const extraCols = ['LCP', 'FCP', 'TTFB'].includes(metric)
? [{ key: 'ttfb', label: 'TTFB' }, { key: 'fcp', label: 'FCP' }, { key: 'lcp', label: 'LCP' }].filter(c => c.key !== metric.toLowerCase())
: metric === 'overall'
? [{ key: 'lcp', label: 'LCP' }, { key: 'inp', label: 'INP' }, { key: 'cls', label: 'CLS' }]
: []; // INP/CLS: no extra columns
: metric === 'overall_sn'
? [{ key: 'lcp_sn', label: 'LCP (SN)' }, { key: 'inp_sn', label: 'INP (SN)' }, { key: 'cls_sn', label: 'CLS (SN)' }]
Comment thread
max-ostapenko marked this conversation as resolved.
Outdated
: []; // INP/CLS/individual softnav metrics: no extra columns

// If current sort column is no longer visible, reset to total
const extraKeys = extraCols.map(c => c.key);
if (['lcp', 'inp', 'cls', 'fcp', 'ttfb'].includes(this.sortColumn) && !extraKeys.includes(this.sortColumn)) {
if (['lcp', 'inp', 'cls', 'fcp', 'ttfb', 'lcp_sn', 'inp_sn', 'cls_sn'].includes(this.sortColumn) && !extraKeys.includes(this.sortColumn)) {
this.sortColumn = 'total';
this.sortDir = 'desc';
}
Expand All @@ -171,7 +174,19 @@ class GeoBreakdown {
const v = row.vitals?.find(x => x.name === name)?.[client];
return v?.tested > 0 ? Math.round(v.good_number / v.tested * 100) : null;
};
return { geo, good: d.good_number, total: d.tested, lcp: pctFor('LCP'), inp: pctFor('INP'), cls: pctFor('CLS'), fcp: pctFor('FCP'), ttfb: pctFor('TTFB') };
return {
geo,
good: d.good_number,
total: d.tested,
lcp: pctFor('LCP'),
inp: pctFor('INP'),
cls: pctFor('CLS'),
fcp: pctFor('FCP'),
ttfb: pctFor('TTFB'),
lcp_sn: pctFor('LCP_sn'),
inp_sn: pctFor('INP_sn'),
cls_sn: pctFor('CLS_sn')
};
}).filter(e => e.total > 0);

// Sort by origins, limit to top 10 unless showAll
Expand Down
Loading