Last active
June 24, 2023 18:54
-
-
Save mbaumbach/c6a2f8e68ae658a4147015da73f9bb3b to your computer and use it in GitHub Desktop.
WebVitalsWidget
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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