• Stars
    star
    3,106
  • Rank 14,474 (Top 0.3 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created almost 7 years ago
  • Updated 6 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Web performance library for measuring all performance vitals metrics
Perfume.js logo

Perfume.js v9.0.0-rc.3

Current version Test Coverage No dependencies Build Status NPM Downloads gzip size brotli size

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.





English | įŽ€äŊ“中文 | Italian | 한ęĩ­ė–´

Why Perfume.js?

Perfume is a tiny, web performance monitoring library that reports field data back to your favorite analytics tool.

  • ⏰ Supports latest Performance APIs for precise metrics
  • 🚀 Device data enrichment
  • 🔨 Cross browser tested
  • đŸšŋ Filters out false positive/negative results
  • 🤙 Only 5.1Kb gzip
  • 🏅 Web Vitals Score
  • 🛰 Flexible analytics tool
  • ⚡ī¸ Waste-zero ms with requestIdleCallback strategy built-in
  • Ability to track data about user actions

The latest in metrics & Real User Measurement

Perfume leverages the latest Performance APIs to collect field data that allows us to understand what real-world users are actually experiencing.

  • Navigation Timing
  • Navigator Interface
  • Resource Timing
  • Element Timing
  • Service Worker Status
  • StorageManager interface
  • Time to First Byte (TTFB)
  • First Contentful Paint (FCP)
  • Largest Contentful Paint (LCP)
  • First Input Delay (FID)
  • Cumulative Layout Shift (CLS)
  • Interaction to Next Paint (INP)
  • Total Blocking Time (TBT)
  • Navigation Total Blocking Time (NTBT)

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.js vs Web Vitals

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.

Installing

npm (https://www.npmjs.com/package/perfume.js):

npm install perfume.js --save

Importing library

You can import the generated bundle to use the whole library generated:

import { Perfume } from 'perfume.js';

Universal Module Definition:

import { Perfume } from 'node_modules/perfume.js/dist/perfume.umd.min.js';

Quick start

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. 🌕

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:

  • deviceMemory: the user's device memory (RAM).
  • hardwareConcurrency: the number of logical CPU processor cores on the user's device.
  • serviceWorkerStatus: status of the service worker between controlled, supported and unsupported.

Based on the Navigator APIs the library can help us differentiate between a low-end and a high-end device/experience:

  • isLowEndDevice: combination of the score of RAM and CPU.
  • isLowEndExperience: combination of the score of RAM, CPU, NetworkStatus and SaveData.

Performance audits

Coo coo coo cool, let's learn something new.

Navigation Timing

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.

  • Redirect time: Page redirects aren't totally inconsequential, but they might not be something you run into very often. Still, redirects add latency to requests, so measuring them may be worth the effort.
  • DNS lookup: When a user requests a URL, the Domain Name System (DNS) is queried to translate a domain to an IP address.
  • Header size: HTTP header size
  • Fetch time: Cache seek plus response time
  • Worker time: Service worker time plus response time
  • Total time: Request plus response time (network only)
  • Download time: Response time only (download)
  • Time to First Byte: The amount of time it takes after the client sends an HTTP GET request to receive the first byte of the requested resource from the server. It is the largest web page load time component taking 40 to 60% of total web page latency.
// Perfume.js: navigationTiming { ... timeToFirstByte: 192.65 }

First Paint (FP)

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

First Contentful Paint (FCP)

FCP is the exact time taken for the browser to render the first bit of content from the DOM, which can be anything from an important image, text, or even the small SVG at the bottom of the page.

// Perfume.js: fcp 2029.00 ms

Largest Contentful Paint (LCP)

LCP is an important, user-centric metric for measuring perceived load speed because it marks the point in the page load timeline when the page's main content has likely loaded—a fast LCP helps reassure the user that the page is useful.

We end the Largest Contentful Paint measure at two points: when First Input Delay happen and when the page's lifecycle state changes to hidden.

// Perfume.js: lcp 2429.00 ms

First Input Delay (FID)

FID measures the time from when a user first interacts with your site (i.e. when they click a link, tap on a button) to the time when the browser is actually able to respond to that interaction.

// Perfume.js: fid 3.20 ms

Cumulative Layout Shift (CLS)

CLS is an important, user-centric metric for measuring visual stability because it helps quantify how often users experience unexpected layout shifts—a low CLS helps ensure that the page is delightful.

We end the Cumulative Layout Shift measure at two points: when First Input Delay happen and when the page's lifecycle state changes to hidden.

// Perfume.js: cls 0.13

Total Blocking Time (TBT)

TBT is an important, user-centric metric for measuring load responsiveness because it helps quantify the severity of how non-interactive a page is prior to it becoming reliably interactive—a low TBT helps ensure that the page is usable.

We end the Total Blocking Time measure 10 seconds after FID.

// Perfume.js: tbt 347.07 ms

Navigation Total Blocking Time (NTBT)

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

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 }

Annotate metrics in the DevTools

Performance.mark (User Timing API) is used to create an application-defined peformance entry in the browser's performance entry buffer.

initPerfume({
  analyticsTracker: ({ metricName, data }) => {
    myAnalyticsTool.track(metricName, data);
  })
});
start('fibonacci');
fibonacci(400);
end('fibonacci');
// Perfume.js: fibonacci 0.14 ms

Performance Mark

Component First Paint

This metric mark the point, immediately after creating a new component, when the browser renders pixels to the screen.

initPerfume({
  analyticsTracker: ({ metricName, data }) => {
    myAnalyticsTool.track(metricName, data);
  })
});
perfume.start('togglePopover');
$(element).popover('toggle');
perfume.endPaint('togglePopover');
// Perfume.js: togglePopover 10.54 ms

Performance

Element Timing

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

User Journey Step Tracking

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])

  ...
}

Defining Steps

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.

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'],
  },
};

initPerfume({ steps });

MarkStep

markStep is the function used to start and end steps in applications.

For example, if we wanted to mark the beginning of load_screen_B step above, we would add in markStep('navigate_to_screen_B') to our code.

enableNavigationTracking

enableNavigationTracking is a boolean in the config that will tell Perfume to take into account page navigation changes or not. By default this is true, but it can be set to false if needed.

The purpose of this feature is to only account for active steps that the user is working on. The feature will remove any inactive or 'stale' steps that are not currently in progress.

Stale steps can be created by navigating away from a page before it fully loads, this would cause the start mark to be triggered, but the end mark to not be called. This would affect the active steps being returned to onMarkStep as well as would create incorrect data if we returned back to the end mark much later than expected.

enableNavigationTracking works together with the incrementUjNavigation function. The incrementUjNavigation function is to be called anytime there is a navigation change in your application. Below is an example for how this would work in a React Application:

import { useLocation } from 'react-router-dom';

const MyComponent = () => {
  const location = useLocation()

  React.useEffect(() => {
    // runs on location, i.e. route, change
    incrementUjNavigation();
  }, [location])
  ...
}

Web Vitals Score

Perfume will expose for all major metrics the vitals score, those can be used to improve your SEO and Google page rank.

Web Vitals Good Needs Improvement Poor
Time to First Byte (TTFB) 0-800 801-1800 Over 1800
Redirect Time (RT) 0-100 101-200 Over 200
First Contentful Paint (FCP) 0-2000 2001-4000 Over 4000
Largest Contentful Paint (LCP) 0-2500 2501-4000 Over 4000
First Input Delay (FID) 0-100 101-300 Over 300
Cumulative Layout Shift (CLS) 0-0.1 0.11-0.25 Over 0.25
Interaction to Next Paint (INP) 0-200 201-500 Over 500
Total Blocking Time (TBT) 0-200 201-600 Over 600
Navigation Total Blocking Time (NTBT) 0-200 201-600 Over 600

Step Tracking is based on various thresholds defined.

Below are the thresholds available for each step:

Label Vital Thresholds
INSTANT [100, 200]
QUICK [200, 500]
MODERATE [500, 1000]
SLOW [1000, 2000]
UNAVOIDABLE [2000, 5000]

Perfume custom options

Default options provided to Perfume.js constructor.

const options = {
  resourceTiming: false,
  elementTiming: false,
  analyticsTracker: options => {},
  maxMeasureTime: 30000,
  enableNavigtionTracking: true,
};

Use Google Analytics

A quick way to see your page speed results on your web app is by using Google Analytics. Those GA events will show on Behavior > Site Speed > User Timings. For testing you might want to see them coming live on Realtime > Events.

Have fun ✨

const metricNames = ['TTFB', 'RT', 'FCP', 'LCP', 'FID', 'CLS', 'TBT'];
initPerfume({
  analyticsTracker: ({ attribution, metricName, data, navigatorInformation, rating, navigationType }) => {
    if (metricNames.includes(metricName)) {
      ga('send', 'event', {
        eventCategory: 'Perfume.js',
        eventAction: metricName,
        // Google Analytics metrics must be integers, so the value is rounded
        eventValue: metricName === 'cls' ? data * 1000 : data,
        eventLabel: navigatorInformation.isLowEndExperience ? 'lowEndExperience' : 'highEndExperience',
        // Use a non-interaction event to avoid affecting bounce rate
        nonInteraction: true,
      });
    }
  })
});

To connect with additional analytics providers, checkout the analytics plugin for Perfume.js.


Develop

  • npm run test: Run test suite
  • npm run build: Generate bundles and typings
  • npm run lint: Lints code

Plugins

Perfume is used by

Credits and Specs

Made with ☕ī¸ by @zizzamia and I want to thank some friends and projects for the work they did:

Contributors

This project exists thanks to all the people who contribute.

Backers

Thank you to all our backers! 🙏 [Become a backer]


Copyright and license

Code and documentation copyright 2022 Leonardo Zizzamia. Code released under the MIT license. Docs released under Creative Commons.

Team


Leonardo Zizzamia

More Repositories

1

ng-tasty

A tasty collection of reusable UI components for Angular, like grandma used to make. Like Table directive, WebSocket / Debounce / Throttle service, Range / Camelize / Slugify filters
JavaScript
434
star
2

a-frame-in-100-lines

Farcaster Frames in less than 100 lines, and ready to be deployed to Vercel.
TypeScript
218
star
3

an-onchain-app-in-100-components

An Onchain App in less than 100 components, and ready to be deployed to Vercel.
TypeScript
11
star
4

san-francisco-the-good-parts

San Francisco: The Good Parts! An opinionated travel guide ☕ī¸ 🌴 ☀ī¸
9
star
5

tasty-flask-app

Beginning Web Dev with Flask, Bootstrap and AngularJS.
Python
8
star
6

generator-benchpress

benchpress
JavaScript
7
star
7

generator-ngtasty

This generator is the scaffolding tool for generate by Yeoman your collection of reusable UI components for AngularJS.
JavaScript
6
star
8

taste-of-angularjs

A Taste of AngularJS: Javascript framework for superheroes
CSS
4
star
9

zizzamia.com

My personal web site in Node.js and CoffeeScript.
JavaScript
4
star
10

angularjs-essentials

AngularJS Essentials
CSS
3
star
11

generator-ng-flask

A tasty scaffold for AngularJS and Flask.
JavaScript
3
star
12

zizzamia.github.io

HTML
2
star
13

angular-mocks

Npm package for angular-mocks.js
2
star
14

bower-ng-tasty

Bower package for the stable branch of ngTasty
JavaScript
2
star
15

building-high-performance-measurable-directives

#ngTasty - Building high performance measurable directives
JavaScript
2
star
16

the-secret-life-cycle-of-components

The Secret Life(cycle) of Components. There is an entire world of complex interactions happening just under the surface of an Angular application.
HTML
2
star
17

rendering-in-angular-2

Angular 2.0 will provide support for custom renderers and platforms. With custom renderers we can have our component/application code render itself differently for a browser, a desktop-level application, a mobile phone or on a watch. We can also leverage native-level code or package our application into a webview.
JavaScript
2
star
18

ultra-simple-blogging

It's an ultra-simple blogging tool with create, edit and delete functionality for articles, using Django on Google App Engine.
Python
1
star
19

talk

JavaScript
1
star
20

angular-2-ngupgrade

Basic migration
JavaScript
1
star
21

angular-app-in-production-lightning-talk

Running an AngularJS App in Production Lightning talk
JavaScript
1
star