Primitives to measure your React application's performance using @shopify/performance
.
First we will need to install the npm package.
yarn add @shopify/react-performance
The @shopify/react-performance
library automatically handles creating a shared Performance
instance (from @shopify/performance
) for you when it is imported. This Performance
instance automatically wraps the browser's built in performance and connection information and calculates some basic page-load stats. As long as we import @shopify/react-performance
somewhere in our app the instance will be there for us.
That said, we will need to add some calls to the usePerformanceMark
hook to our application in order to help @shopify/react-performance
determine when a page is fully loaded and React has finished mounting components.
//Cats.tsx
import React from 'react';
import {usePerformanceMark, Stage} from '@shopify/react-performance';
// Represents an entire page of our application
export function Cats() {
// Tell the @shopify/react-performance the page is completely loaded
usePerformanceMark(Stage.Complete, 'cats');
return 'Kokusho, Moe, The Dude';
}
note: In production apps, it is important to always have one usePerformanceMark
on each page level component. Otherwise, client-side navigations may not be recorded properly. When usePerformanceMark
calls are omitted in some pages but not others, it is common to see strange results such as:
- Navigation data which makes it look like some page takes infinitely long to load
- Navigation data in which some page sometimes randomly takes several times longer than usual
- Missing data for a particular page
To demonstrate the data we have so far, we'll create a component called LastNavigationDetails
and use it to display some basic data about our app's performance.
// LastNavigationDetails.tsx
import React, {useState} from 'react';
import {useNavigationListener, Navigation} from '@shopify/react-performance';
// A component which displays simple performance stats about the last navigation performed by the user
export function LastNavigationDetails() {
// create some state to hold the last navigation
const [lastNavigation, setLastNavigation] = useState<Navigation | null>(null);
// listen for subsequent client-side navigations and update our state
useNavigationListener((navigation) => {
setLastNavigation(navigation);
});
if (lastNavigation == null) {
return <p>no data</p>;
}
const {duration, isFullPageNavigation, target} = lastNavigation;
// output some information about the last navigation
const navigationType = isFullPageNavigation
? 'full page navigation'
: 'single-page-app style navigation';
return (
<p>
The last navigation was to {target}. It was a {navigationType} navigation
which took {duration / 1000} seconds to complete.
</p>
);
}
We can render this component anywhere in our application, but lets do so in our top level App component.
// App.tsx
import React from 'react';
import {PerformanceMark} from '@shopify/react-performance';
// our component from above which calls `useNavigationListener`
import {LastNavigationDetails} from './LastNavigationDetails';
// our component from above which calls usePerformanceMark
import {Cats} from './Cats';
// the top level component of our application
function App() {
return (
<>
{/* render our LastNavigationDetails component to give devs some useful stats */}
<LastNavigationDetails />
{/* render the "Cats" page */}
<Cats />
</>
);
}
Now our developers will have access to up-to-date information about how long initial page-loads and incremental navigations take to complete without needing to open their devtools or dig into DOM APIs manually.
Performance metrics data is only truly useful when it can be aggregated and used to make smart optimization decisions or warn you when your app is beginning to become slow. To aggregate such data you'll need to be able to send it to a server. @shopify/react-performance
provides the usePerformanceReport
hook to facilitate this.
// App.tsx
import React from 'react';
import {
PerformanceMark,
usePerformanceReport,
} from '@shopify/react-performance';
/**
* The top level component of our application, it should handle rendering all top level providers and determining what pages to render
*/
function App() {
// send a performance report to `/performance_report`
usePerformanceReport('/performance_report');
return <>{/* rest of the app */}</>;
}
Assuming our App sets up some <PerformanceMark />
components, our code will send a performance report to the /performance_report
url as JSON. Generally, the easiest way to set up a server to make use of the reported data is to use one of our server-side companion libraries.
For Node services based on Koa
, we provide @shopify/koa-performance to parse PerformanceReport
payloads and send them forward to a StatsD endpoint as distribution
s.
For Rails services we provide quilt_rails's Quilt::Performance::Reportable
mixin for parsing PerformanceReport
payloads and sending them forward to a StatsD endpoint as distribution
s.
A custom hook which takes in callback to invoke whenever the Performance
context object emits a lifecycleEvent.
import {useLifecycleEventListener} from '@shopify/react-performance';
function SomeComponent() {
useLifecycleEventListener((event) => {
console.log(event);
});
return <div>something</div>;
}
A custom hook which takes in a callback to invoke whenever a navigation takes place, whether it is a full-page load (such as the result of location.assign
) or an incrmental load (such as the result of router.push
in a ReactRouter app).
import {useNavigationListener} from '@shopify/react-performance';
function SomeComponent() {
useNavigationListener((navigation) => {
console.log(navigation);
});
return <div>something</div>;
}
A component which takes a stage
and an id
and uses them to generate a tag for a call to window.performance.mark
. This can be used to mark specialized moments in your applications startup, or a specific interaction.
import React from 'react';
import {usePerformanceMark} from '@shopify/react-performance';
function SomeComponent() {
usePerformanceMark('some-stage', 'some-id');
return <div>something</div>;
}
Though you can use usePerformanceMark
with any arbitrary string passed as the stage
parameter, two special values exist with predetermined meanings and extra side-effects. These values are contained in the Stage
enum.
-
Stage.Usable
: Marks the page usable usingperformance.usable()
. A page should be consideredusable
when the bare-minimum of functionality is present. Usually this just means when the React application has mounted the page, but the application is still waiting on some asynchronous data. -
Stage.Complete
: Marks the page complete usingperformance.finish()
. A page should be consideredcomplete
when all data is fetched and the page is fully rendered.
For page level components, you should always have at least a usePerformanceMark
call with a stage
of Stage.Complete
.
import React from 'react';
import {usePerformanceMark, Stage} from '@shopify/react-performance';
function CatsPage() {
// mark the component as complete immediately upon rendering
usePerformanceMark(Stage.Complete, 'ProductPage');
return 'Kokusho, Moe, Smoky, Mama Cat';
}
In a more complex application with calls for remote data or other asynchronous work which blocks some portion of the tree from rendering it is common to need more granular data about the lifecycle of a page load. To gather this data we can make the stage
parameter we pass to usePerformanceMark
dynamic, and give it complete
only when data has loaded.
// ProductPage.tsx
import React, {useState, useEffect} from 'react';
import {usePerformanceMark, Stage} from '@shopify/react-performance';
import {useRemoteKitties} from './hooks';
export function CatsPage() {
// get some data from a remote API
const {data, error} = useRemoteKitties();
// we consider the page complete when we have either our data, or the call for data has returned an error.
const stage = data || error ? Stage.Complete : Stage.Usable;
usePerformanceMark(stage, 'CatsPage');
return data || error;
}
A custom hook which takes a url and sends a detailed breakdown of all navigations and events which took place while the page was loading. This hook should generally only be used once at the top of your component tree.
// App.tsx
import React from 'react';
import {usePerformanceReport} from '@shopify/react-performance';
import {ProductPage} from './ProductPage';
function App() {
usePerformanceReport('/performance-report');
return <ProductPage />;
}
To get sensical data, applications using usePerformanceReport
should be sure to have at least one performance mark on each top level page.
usePerformanceReport
accepts a locale
property that will be associated with all events.
// App.tsx
import React from 'react';
import {usePerformanceReport} from '@shopify/react-performance';
import {ProductPage} from './ProductPage';
function App() {
usePerformanceReport('/performance-report', {locale: navigator.language});
return <ProductPage />;
}
By default, usePerformanceReport
will only report on navigations that were "Finished" (ie. a performance mark with Stage.Complete was rendered). If you want to include Navigations that were "Cancelled" or "Timed Out" you can specify those.
// App.tsx
import React from 'react';
import {NavigationResult} from '@shopify/performance';
import {usePerformanceReport} from '@shopify/react-performance';
import {ProductPage} from './ProductPage';
function App() {
usePerformanceReport('/performance-report', {finishedNavigationsOnly: false});
return <ProductPage />;
}
This library also provides the following component implementations of the above hooks:
-
<NavigationListener />
, the component version ofuseNavigationListener
-
<LifecycleEventListener />
, the component version ofuseLifecycleEventListener
-
<PerformanceReport />
, the component version ofusePerformanceReport
-
<PerformanceMark />
, the component version ofusePerformanceMark
The components are provided primarily for backwards compatibility, and the hook implementations should be preferred.
This package also re-exports all of the API from @shopify/performance
.