Page Speed is a feature, and to deliver it we need to understand the many factors and fundamental limitations that are at play. If we can measure it, we can improve it.
Perfume is a tiny, web performance monitoring library that reports field data back to your favorite analytics tool.
Perfume leverages the latest Performance APIs to collect field data that allows us to understand what real-world users are actually experiencing.
At Coinbase, we use Perfume.js to capture a high-level scoring system that is clear, trusted, and easy to understand.
Summarizing the performance health of an application into a reliable and consistent score helps increase urgency and directs company attention and resources towards addressing each performance opportunity.
Perfume leverages the Web Vitals library to collect all the standardized performance metrics. It explores new metrics like Navigation Total Blocking Time and dimensions like Low-End Devices, Service Worker status to understand your data better.
So don't worry, Perfume.js is a superset of Web Vitals, a bit like Typescript is a superset of Javascript.
npm (https://www.npmjs.com/package/perfume.js):
npm install perfume.js --save
You can import the generated bundle to use the whole library generated:
import { initPerfume } from 'perfume.js';
Universal Module Definition:
import { initPerfume } from 'node_modules/perfume.js/dist/perfume.umd.min.js';
Metrics like Navigation Timing, Network Information, TTFB, FCP, FID, LCP, CLS, INP and TBT are default reported with Perfume; All results will be reported to the analyticsTracker callback, and the code below is just one way for you to organize your tracking, feel free to tweak it suit your needs.
🚀 Visit perfumejs.com for a live demo on how the metrics work. 🌕
import { initPerfume } from 'perfume.js';
initPerfume({
analyticsTracker: options => {
const {
attribution,
metricName,
data,
navigatorInformation,
rating,
navigationType,
} = options;
switch (metricName) {
case 'navigationTiming':
if (data && data.timeToFirstByte) {
myAnalyticsTool.track('navigationTiming', data);
}
break;
case 'networkInformation':
if (data && data.effectiveType) {
myAnalyticsTool.track('networkInformation', data);
}
break;
case 'storageEstimate':
myAnalyticsTool.track('storageEstimate', data);
break;
case 'TTFB':
myAnalyticsTool.track('timeToFirstByte', { duration: data });
break;
case 'RT':
myAnalyticsTool.track('redirectTime', { duration: data });
break;
case 'FCP':
myAnalyticsTool.track('firstContentfulPaint', { duration: data });
break;
case 'FID':
myAnalyticsTool.track('firstInputDelay', { duration: data });
break;
case 'LCP':
myAnalyticsTool.track('largestContentfulPaint', { duration: data });
break;
case 'CLS':
myAnalyticsTool.track('cumulativeLayoutShift', { value: data });
break;
case 'INP':
myAnalyticsTool.track('interactionToNextPaint', { value: data });
break;
case 'TBT':
myAnalyticsTool.track('totalBlockingTime', { duration: data });
break;
case 'elPageTitle':
myAnalyticsTool.track('elementTimingPageTitle', { duration: data });
break;
case 'userJourneyStep':
myAnalyticsTool.track('userJourneyStep', {
duration: data,
stepName: attribution.step_name,
vitals_score: rating,
});
break;
default:
myAnalyticsTool.track(metricName, { duration: data });
break;
}
},
});
In a world with widely varying device capabilities, a one-size-fits-all event doesn’t always work. Perfume adds data enrichment to all events so we can better understand the real world experiences:
Based on the Navigator APIs the library can help us differentiate between a low-end and a high-end device/experience:
Coo coo coo cool, let's learn something new.
Navigation Timing collects performance metrics for the life and timings of a network request.
Perfume helps expose some of the key metrics you might need.
// Perfume.js: navigationTiming { ... timeToFirstByte: 192.65 }
FP is the exact time taken for the browser to render anything as visually different from what was on the screen before navigation, e.g. a background change after a long blank white screen time.
// Perfume.js: fp 1482.00 ms
This metric measures the amount of time the application may be blocked from processing code during the 2s window after a user navigates from page A to page B. The NTBT metric is the summation of the blocking time of all long tasks in the 2s window after this method is invoked.
Because this library is navigation agnostic, we have this method to mark when the navigation starts.
If this method is called before the 2s window ends; it will trigger a new NTBT measurement and interrupt the previous one.
import { createBrowserHistory } from 'history';
export const history = createBrowserHistory();
initPerfume({
analyticsTracker: ({ metricName, data }) => {
myAnalyticsTool.track(metricName, data);
})
});
// React custom history
history.listen(() => {
// Measure NTBT at the beginning of each navigation
perfume.markNTBT();
});
// Perfume.js: ntbt 78 ms
Resource Timing collects performance metrics for document-dependent resources. Stuff like style sheets, scripts, images, et cetera. Perfume helps expose all PerformanceResourceTiming entries and groups data data consumption by Kb used.
initPerfume({
resourceTiming: true,
analyticsTracker: ({ metricName, data }) => {
myAnalyticsTool.track(metricName, data);
})
});
// Perfume.js: dataConsumption { "css": 185.95, "fetch": 0, "img": 377.93, ... , "script": 8344.95 }
Performance.mark (User Timing API) is used to create an application-defined peformance entry in the browser's performance entry buffer.
import { start, end } from 'perfume.js';
initPerfume({
analyticsTracker: ({ metricName, data }) => {
myAnalyticsTool.track(metricName, data);
})
});
start('fibonacci');
fibonacci(400);
end('fibonacci');
// Perfume.js: fibonacci 0.14 ms

Track when image elements and text nodes are displayed on screen using the emerging Element Timing API specification by simply adding the elementtiming attribute with a descriptive value of your choice to HTML elements you would like to measure:
<h1 elementtiming="elPageTitle" class="title">Perfume.js</h1>
<img
elementtiming="elHeroLogo"
alt="Perfume.js logo"
src="https://zizzamia.github.io/perfume/assets/perfume-logo-v5-0-0.png"
/>
initPerfume({
elementTiming: true,
analyticsTracker: ({ metricName, data }) => {
myAnalyticsTool.track(metricName, data);
})
});
// Perfume.js: elPageTitle 256.00 ms
// Perfume.js: elHeroLogo 1234.00 ms
A Step represents a slice of time in the User Journey where the user is blocked by system time. System time is time the system is blocking the user. For example, the time it takes to navigate between screens or fetch critical information from the server. This should not be confused with cognitive time, which is the time the user spends thinking about what to do next. User Journey steps should only cover system time.
A Step is defined by an event to start the step, and another event to end the step. These events are referred to as Marks.
As an example, a Step could be to navigate from screen A to screen B. The appropriate way to mark a start and end to this step is by marking the start when tapping on the button on screen A that starts the navigation and marking the end when screen B comes into focus with the critical data rendered on the screen.
// Marking the start of the step
const ScreenA = () => {
const handleNavigation = () => {
... // Navigation logic
// Mark when navigating to screen B
markStep('navigate_to_screen_B');
}
...
return (
<>
<Button onPress={handleNavigation} />
</>
);
}
// Marking the end of the step
const ScreenB = () => {
const { viewer } = fetch("http://example.com/userInfo")
.then((response) => response.json())
.then((data) => data);
const {name} = viewer.userProperties;
useEffect(() => {
if (name) {
// Mark when data is ready for screen B
markStep('loaded_screen_B');
}
}, [name])
...
}
In order for Perfume to be able to track metrics for Steps, we need to configure the steps and provide them when initializing Perfume.
Below you can find an example of how to do this.
```typescript export const steps = { load_screen_A: { threshold: ThresholdTier.quick, marks: ['navigate_to_screen_A', 'loaded_screen_A'], }, load_screen_B: { threshold: ThresholdTier.quick, marks: ['navigate_to_screen_B', 'loaded_screen_B'], }, };
initPerfum
$ claude mcp add perfume.js \
-- python -m otcore.mcp_server <graph>