Skip to content

Instantly share code, notes, and snippets.

@mbaumbach
Last active June 24, 2023 18:54
Show Gist options
  • Select an option

  • Save mbaumbach/c6a2f8e68ae658a4147015da73f9bb3b to your computer and use it in GitHub Desktop.

Select an option

Save mbaumbach/c6a2f8e68ae658a4147015da73f9bb3b to your computer and use it in GitHub Desktop.
WebVitalsWidget
// Requires: web-vitals package to be added to your package.json dependencies.
// Usage: <WebVitalsWidget enabled />
// Ideally you should mount this component as high as you can in your component hierarchy so it renders ASAP.
// You can set the enabled prop to use an environment variable to make sure it's only on in dev/staging and off in prod if you want.
// You can add a query parameter of __vitals=true to the end of your URL at any time and it will trigger the widget to show.
// If the widget is covering an important part of your app, you can move it around with the location prop.
// Some data won't populate in the widget until you click somewhere on the page to bring focus to the window (LCP/FID)
// The CLS data will populate if you click to another tab and come back in.
// The LCP and CLS will have underlines on them and can be clicked to reveal the element that's impacting the score the most.
import type { MouseEvent } from 'react';
import { useEffect, useState, type PropsWithChildren } from 'react';
import type { CLSMetricWithAttribution, FIDMetricWithAttribution, LCPMetricWithAttribution } from 'web-vitals/attribution';
import { onCLS, onFID, onLCP } from 'web-vitals/attribution';
export type WebVitalsWidgetProps = {
enabled: boolean;
location?: 'nw' | 'ne' | 'sw' | 'se';
};
export function WebVitalsWidget({ enabled, location = 'se' }: WebVitalsWidgetProps) {
const [lcp, setLcp] = useState<LCPMetricWithAttribution | undefined>();
const [fid, setFid] = useState<FIDMetricWithAttribution | undefined>();
const [cls, setCls] = useState<CLSMetricWithAttribution | undefined>();
const searchParams = new URLSearchParams(window.location.search);
const isVisible = enabled || searchParams.get('__vitals') === 'true';
useEffect(() => {
if (!isVisible) {
return;
}
onLCP(l => 'attribution' in l && setLcp(l));
onFID(f => 'attribution' in f && setFid(f));
onCLS(c => 'attribution' in c && setCls(c));
}, [isVisible]);
if (!isVisible) {
return null;
}
return (
<div
style={{
display: 'block',
position: 'fixed',
top: location === 'ne' || location === 'nw' ? 0 : 'auto',
left: location === 'nw' || location === 'sw' ? 0 : 'auto',
bottom: location === 'se' || location === 'sw' ? 0 : 'auto',
right: location === 'se' || location === 'ne' ? 0 : 'auto',
margin: '20px',
padding: '10px',
backgroundColor: 'white',
color: 'black',
borderRadius: '6px',
boxShadow: '0px 1px 3px 0px #666',
zIndex: 2147483000
}}>
<h3 style={{ marginBottom: '5px', fontWeight: 'bold' }}>Web vitals</h3>
<div>
<strong>LCP:</strong> {lcp ? <MetricElement metric={lcp}>{(lcp.value / 1000).toFixed(2)}s</MetricElement> : 'No data'}
</div>
<div>
<strong>FID:</strong> {fid ? <MetricElement metric={fid}>{fid.value.toFixed(1)}ms</MetricElement> : 'No data'}
</div>
<div>
<strong>CLS:</strong> {cls ? <MetricElement metric={cls}>{cls.value.toFixed(2)}</MetricElement> : 'No data'}
</div>
</div>
);
}
type MetricElementProps = PropsWithChildren & {
metric: LCPMetricWithAttribution | FIDMetricWithAttribution | CLSMetricWithAttribution;
};
function MetricElement({ metric, children }: MetricElementProps) {
let element: string | undefined;
if ('element' in metric.attribution) {
element = metric.attribution.element;
} else if ('largestShiftTarget' in metric.attribution) {
element = metric.attribution.largestShiftTarget;
}
function onClick(e: MouseEvent<HTMLAnchorElement>) {
e.preventDefault();
const foundElement = element ? document.querySelector<HTMLElement>(element) : null;
if (foundElement) {
foundElement.style.border = 'solid 1px red';
console.log(foundElement);
}
}
return (
<span
style={{
textDecoration: element ? 'underline' : 'none',
cursor: element ? 'pointer' : 'default',
color: metric.rating === 'good' ? 'rgb(0, 128, 96)' : metric.rating === 'needs-improvement' ? 'rgb(145, 106, 0)' : 'red'
}}
onClick={onClick}>
{children}
</span>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment