More than once you’ve heard people talk about performance, how important a fast Web application is.
Is my website fast? When you try to answer this question, you’ll find that fast is a very vague concept. When we say fast, what exactly are we talking about? Under what circumstances? For whom?
It’s important to be accurate when talking about performance and not use the wrong concepts, lest developers keep optimizing for the wrong things — and end up hurting the user experience instead of getting optimized.
To take a specific example, we often hear people say: my app was tested and the load time was XX seconds.
None of this is to say that it is wrong, but that it distorts reality. Load times vary from user to user, depending on their device capabilities and network environment. Focusing on load times alone will miss users with long load times.
In fact, the load time mentioned above is an average load time for all users. Only the histogram distribution shown below can fully reflect the true load time:
The number on the X-axis represents the load time, and the height of the histogram on the Y-axis represents the number of users at a given time. As you can see, while most users have a load time of 1 to 2 seconds, there are still many users who have a long load time.
Another reason why “my page loaded in xx seconds” isn’t true is that page loading isn’t a single time metric — it’s the user experience, and that experience is something that no metric can fully capture. There are several times during loading that can affect the user’s perception of whether or not a page is loading fast enough, and if you focus solely on the time it takes to complete a load, you may overlook the poor user experience that happens at other points in time.
For example, the initial rendering of an application is optimized so quickly that the page content is quickly presented to the user. If the application then loads a large JS package that takes several seconds to parse and execute, the page content will still not respond to the user’s actions until the JS execution is complete. If a user sees a link and can’t click it, or has a text box and can’t enter it, they probably don’t care how fast your page renders.
So instead of using a single metric to measure the speed of loading, we should focus on any time metrics that affect how the user feels throughout the loading process.
The second misconception is that performance is a concern only at load time.
We made a mistake here as a team, and it was amplified by the fact that most performance monitoring tools only look at load performance.
In fact, performance problems can occur at any time, not just at load time. The user clicks slowly, the page does not scroll, and the animation is not smooth. Users care about the overall experience, and so should we as developers.
What these myths have in common is that the metrics we focus on have little or no relationship to user experience. Also, traditional performance metrics such as page load time, DOMContentLoaded time, are very unreliable. Because, when they show up, it doesn’t mean the user thinks the app has already loaded.
So to make sure we don’t repeat this mistake, we ask ourselves a few questions:
- What metrics best reflect the intuitive user experience?
- How do you detect these metrics in real users?
- How do you share the data to see if your app is fast enough?
- Understanding what real user performance of an application is, how can we prevent performance degradation? How to optimize in the future?
User-centered performance indicators
When a user visits a page, it is often visually aware that the page has loaded as expected and is ready to use.
The theme | instructions |
---|---|
Did it happen? | Does the page start to jump? Is the server responding? |
Does the content matter? | Is the important content rendered? |
Can I use it? | Is the page interactive? Or is it still loading? |
Was the experience good? | Is the interaction smooth and natural, with no lag? |
To understand how the page performs in these aspects on the user side, we define some metrics:
First drawing and first drawing with content
The Paint Timing interface defines two metrics: first draw (FP) and first content draw (FCP). These metrics record the point in time when the browser starts to draw on the screen. This is important to the user because it answers: “Did it happen?” This question.
The main difference between the two metrics is that FP is the point in time when the page first visually appears different from what it was before the jump. In contrast, FCP is when the browser renders the first content in the DOM, which could be text, images, SVG, or even a < Canvas > element.
First meaningful rendering and key element rendering time
The first useful drawing answers the question: “Does it work?” . There is no standard definition of what is useful. But it’s easy for developers to figure out which parts of the page are most important to users.
There is almost always more important content on a web page than anything else. If the most important part of a web page loads quickly, users may not even notice if the rest of the page is not loading.
Long task
The browser responds to user input by adding tasks to the queue on the main thread and executing them one by one. This is also where the browser executes JavaScript, so the browser is single-threaded in that sense.
In some cases, these tasks may take a long time to complete, in which case the main thread will be blocked and all other tasks in the queue will have to wait.
The long task API recognizes any task longer than 50 milliseconds, which it considers a performance hazard. With the long Tasks API, developers can access the long tasks that exist on the page. 50ms was selected in order to follow the RAIL guidelines to ensure that user input is responded to within 100ms.
Interactivity time
Interactivity Time (TTI) means that the page is rendered and ready to respond to user input. There may be several reasons why a page cannot respond to user input:
- Make sure that the interactive JS for the page is not downloaded
- Long tasks exist (described in the previous section)
TTI represents the point at which the page’s initial JavaScript load is complete and the main thread is free (no long tasks).
Correlate metrics to user experience
Going back to the issues we previously considered most important to ux, this table Outlines how each of the metrics we just listed maps to the ux we want to optimize:
experience | indicators |
---|---|
Did it happen? | First Draw (FP)/First Content Draw (FCP) |
Does the content matter? | First useful draw (FMP)/key element render time |
Can I use it? | Interactionable time (TTI) |
Was the experience good? | Long task |
A screenshot of the page loading timeline can help you better identify where these metrics are in the loading process.
Measure these metrics on real users’ devices
One of the main reasons we have traditionally optimized metrics like Load and DOMContentLoaded is that they are easy to measure on real users as events in the browser.
By contrast, many other indicators have historically been hard to measure. For example, we often see developers use this middle line of code to measure long tasks:
(function detectLongFrame() {
var lastFrameTime = Date.now();
requestAnimationFrame(function() {
var currentFrameTime = Date.now();
if(currentFrameTime - lastFrameTime > 50) { // Report long frame here... } detectLongFrame(currentFrameTime); }); } ());Copy the code
This code uses the requestAnimationFrame loop to record the time of each iteration. If the current time is 50 milliseconds longer than the previous time, it is considered a long task. While this code works, it has many disadvantages:
- Increased overhead per frame.
- Block idle time blocks.
- The battery life is affected. The most important principle of performance detection code is not to make performance worse.
Lighthouse and Web Page Test have been providing these new performance metrics for some time now (they’re great tools for pre-launch performance testing), but since they don’t run on user devices, they still don’t measure how well Web projects are actually performing on user devices.
Fortunately, browsers offer new apis that make it easy to measure the performance of real users’ devices without the need for workarounds that affect page performance.
These new apis are PerformanceObserver, PerformanceEntry and DOMHighResTimeStamp. Let’s look at an example of how to use PerformanceObserver to measure performance related to drawing (for example, FP, FCP) and possible js long tasks that cause page blocking.
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// `entry` is a PerformanceEntry instance.
console.log(entry.entryType);
console.log(entry.startTime); // DOMHighResTimeStamp
console.log(entry.duration); // DOMHighResTimeStamp
}
});
// Start observing the entry types you care about.
observer.observe({entryTypes: ['resource'.'paint']});
Copy the code
PerformanceObserver allows you to subscribe to performance events and get the data as they occur. Compared to the older PerformanceTiming interface, it has the advantage of getting data asynchronously rather than through continuous polling.
FP/FCP statistics
After obtaining certain performance data, you can send the data to any data analysis service. Let’s say we send the metrics we first drew to Google for statistics.
<head> <! -- Add the async Google Analytics snippet first. --> <script> window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)}; ga.l=+new Date; ga('create'.'UA-XXXXX-Y'.'auto');
ga('send'.'pageview');
</script>
<script async src='https://www.google-analytics.com/analytics.js'></script> <! -- Register the PerformanceObserver to track paint timing. --> <script> const observer = new PerformanceObserver((list) = > {for (const entry of list.getEntries()) {
// `name` will be either 'first-paint' or 'first-contentful-paint'.
const metricName = entry.name;
const time = Math.round(entry.startTime + entry.duration);
ga('send'.'event', {
eventCategory: 'Performance Metrics',
eventAction: metricName,
eventValue: time,
nonInteraction: true}); }}); observer.observe({entryTypes: ['paint']}); </script> <! -- Include any stylesheets after creating the PerformanceObserver. --> <link rel="stylesheet" href="...">
</head>
Copy the code
FMP statistics based on key key elements
We do not yet have a standardized definition of FMP (and therefore no corresponding performance type). This is partly because it is difficult to have a common metric to say that all pages are “meaningful.”
However, in a single-page application scenario, we can represent FMP in terms of the point in time that the key elements are displayed.
Steve Souders has a great article called User Timing And Custom Metrics that details a number of techniques for using browser performance apis to determine when you can see various types of media.
Statistical TTI
In the long term, we hope to provide standardized support for TTI metrics in the browser through PerformanceObserver. At the same time, we developed a Polyfill that can be used to detect TTI and works in any browser that supports the long-task API.
The polyfill exposes a getFirstConsistentlyInteractive () method, this method returns a promise by parsing the TTI value object. You can calculate TTI using Google Analytics, as shown below:
import ttiPolyfill from './path/to/tti-polyfill.js';
ttiPolyfill.getFirstConsistentlyInteractive().then((tti) => {
ga('send'.'event', {
eventCategory: 'Performance Metrics',
eventAction: 'TTI',
eventValue: tti,
nonInteraction: true}); });Copy the code
GetFirstConsistentlyInteractive () method accepts an optional startTime configuration option, to specify a time represents the web application cannot interact before this time. By default, a Polyfill uses DOMContentLoaded as the start time, but it’s often more accurate to use something like the moment when a key element is visible or when all event listeners are known to have been added.
Refer to the TTI Polyfill documentation for complete installation and use instructions.
Statistical long task
I mentioned earlier that long tasks can lead to some negative user experience (for example, slow event handlers, lost frames). It’s best to keep an eye on how often long tasks occur to minimize their impact.
To detect long tasks in JavaScript, create a PerformanceObserver object and observe the LongTask type. One advantage of the long task type is that it includes a Attribution property, so it makes it easier to keep track of which code is causing the long task:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
ga('send'.'event', {
eventCategory: 'Performance Metrics',
eventAction: 'longtask', eventValue: Math.round(entry.startTime + entry.duration), eventLabel: JSON.stringify(entry.attribution), }); }}); observer.observe({entryTypes: ['longtask']});
Copy the code
The Attribution property tells you what code is causing the long task, which can help you determine if the third-party Iframe script is causing the problem. Future versions of the specification are planned to add more granularity and provide script urls, row and column numbers, which will be helpful to determine if your scripts are causing slowness.
Statistical input delay
Long tasks that block the main thread prevent your event listeners from executing in a timely manner. The RAIL performance model tells us that in order for the user interface to feel smooth, it should respond within 100 milliseconds of user input, and otherwise analyze why.
To detect input latency in your code, you can compare the timestamp of the event to the current time, and if the difference is greater than 100 milliseconds, you can (and should) report an exception.
const subscribeBtn = document.querySelector('#subscribe');
subscribeBtn.addEventListener('click', (event) => {
// Event listener logic goes here...
const lag = performance.now() - event.timeStamp;
if (lag > 100) {
ga('send'.'event', {
eventCategory: 'Performance Metric'
eventAction: 'input-latency',
eventLabel: '#subscribe:click',
eventValue: Math.round(lag),
nonInteraction: true}); }});Copy the code
Since event latency is usually a result of long tasks, you can combine the event latency detection logic with the long task attribution logic: if both the long task and event.timestamp block the main thread, you can also report the long task Attribution value. This can determine what code is causing the poor performance experience.
While this technique isn’t perfect (it doesn’t handle long event listeners in the bubble phase, and it doesn’t work for scrolling or composite animations that aren’t running on the main thread), it’s the first step to better understanding how long running JavaScript code can affect the user experience.
Data interpretation
Once you start collecting performance metrics from real users, you need to put that data into practice. Real user performance data is important for several reasons:
- Verify that your application is performing as expected.
- Find out how poor performance affects conversion rates (whatever conversion rates mean to your application).
- Look for measures to improve the user experience. The performance of your app on mobile and desktop devices is definitely something to compare. The following image shows the TTI distribution for desktop (blue) and mobile (orange). As you can see from this example, the TTI value on the phone is much longer than the TTI value on the desktop:
While the data here is application-specific (you should test the data for your own application), the following example is an analysis report generated based on performance metrics:
The desktop
Percentile | TTI (seconds) |
---|---|
50% | 2.3 |
75% | 4.7 |
90% | 8.3 |
The mobile terminal
Percentile | TTI (seconds) |
---|---|
50% | 3.9 |
75% | 8.0 |
90% | 12.6 |
By splitting data into mobile and desktop, and mapping the data from each terminal, you can quickly gain insight into the experience of real users. For example, looking at the table above, it’s easy to see that for this application, 10% of mobile users spend more than 12 seconds interacting!
How does performance affect services
The user jumps out during loading
We know that if a page takes too long to load, users will often leave. This means that all of our performance metrics have a survivability bias problem, and the data doesn’t include the numbers of people who don’t wait for the page to finish loading.
While you can’t get data on the retention of these users, you can get data on how often it happens and how long each user stays.
This is a bit tricky to use with Google Analytics because the Analytics. Js library is usually loaded asynchronously and may not be available when the user decides to leave. However, you don’t need to wait for Analytics. Js to load before sending data to Google Analytics. You can send it directly through the Measurement Protocol.
This code listens for a VisibilityChange event (which is triggered if the current page is running in the background or if the page is closed) and sends the value of performanc.now () when the event is triggered.
<script>
window.__trackAbandons = () => {
// Remove the listener so it only runs once.
document.removeEventListener('visibilitychange', window.__trackAbandons);
const ANALYTICS_URL = 'https://www.google-analytics.com/collect'; const GA_COOKIE = document.cookie.replace( /(? : (? : ^ |. *) \s*_ga\s*\=\s*(? :\w+\.\d\.) ([^;] *). * $) | ^. * $/,'$1');
const TRACKING_ID = 'UA-XXXXX-Y';
const CLIENT_ID = GA_COOKIE || (Math.random() * Math.pow(2, 52));
// Send the data to Google Analytics via the Measurement Protocol.
navigator.sendBeacon && navigator.sendBeacon(ANALYTICS_URL, [
'v=1'.'t=event'.'ec=Load'.'ea=abandon'.'ni=1'.'dl=' + encodeURIComponent(location.href),
'dt=' + encodeURIComponent(document.title),
'tid=' + TRACKING_ID,
'cid=' + CLIENT_ID,
'ev=' + Math.round(performance.now()),
].join('&'));
};
document.addEventListener('visibilitychange', window.__trackAbandons);
</script>
Copy the code
You can copy this code into the of the document and replace the UA-xxX-y placeholder with your track ID.
You also need to make sure that this listener is removed when the page becomes interactive, otherwise you will mistakenly report aborted loading and waiting for work when reporting TTI.
document.removeEventListener('visibilitychange', window.__trackAbandons);
Copy the code
Performance optimization and performance degradation prevention
The benefit of defining user-centered metrics is that when optimized for them, it inevitably leads to an improvement in the user experience.
One of the easiest ways to improve performance is to only send less JavaScript code to the client, but without reducing the size of the code, it’s critical to think about how the JavaScript is delivered.
Optimization of FP/FCP
You can shorten the time to first draw and first content draw by removing any scripts or stylesheets that block rendering from the of the document.
Take the time to determine the minimal set of styles needed for the user to perceive “it’s happening” and inline it to the (or push it through the HTTP2 service) and you’ll get incredibly fast first draw times.
The App Shell pattern applied in the PWA is a good example.
Optimization of FMP/TTI
Once you have identified the most critical UI elements on the page, you should make sure that the initial script that you load contains only the code to render and interact with those elements properly.
Any code that is unrelated to the key elements that is included in the initial JS module will slow down the interaction time. There’s no reason to force users to download and parse js code they don’t need for a while.
As a general rule of thumb, you should compress the time interval between FMP and TTI as much as possible. If compression is not possible, it is necessary to clearly indicate to the user that the current user cannot interact.
One of the most annoying experiences for users is when they click on an element and nothing happens.
Prevent long tasks
Js code segmentation, optimize the load order of JS, can not only make the page interaction time faster, but also reduce the long task, reduce the input delay and slow frame caused by long task.
In addition to splitting the code into separate files, you can break large chunks of code that are synchronized into smaller chunks that execute asynchronously, or delay until the next idle point. By executing this logic asynchronously in smaller blocks of code, you can leave room on the main thread for the browser to respond to user input.
Finally, you should make sure that the referenced third-party code is tested for long tasks. Third party ads or statistics scripts that lead to a lot of long tasks will ultimately hurt your business.
Prevent performance degradation
This article focuses on real user performance measurements, and while real user data is the ultimate performance data to focus on, testing environment data is critical to ensuring that your application performs well (and doesn’t degrade) before releasing new features. The test phases are ideal for degradation detection because they run in a controlled environment and are not susceptible to the random variability of a real user environment.
Tools like Lighthouse and Web Page Test can be integrated into a continuous integration server and can fail a build if key metrics degrade or drop below certain thresholds.
For published code, you can add custom alerts that notify you when performance metrics are deteriorating. For example, if a third party releases new code and your users suddenly have a lot of long tasks, you will be alerted.
To successfully prevent performance degradation, you need to test and perform performance tests in real user environments with each new feature release.
Summary and Outlook
In the last year, we’ve made significant progress in opening up user-centric metrics to developers on the browser, but we’re not done yet, and there’s more planned to do.
We really want to standardize the statistics of interaction times and key element display times so that developers don’t have to calculate them themselves or rely on Polyfills to do so. We also want to make it easier for developers to locate long tasks and specific code locations that cause frame loss and input delays.
While we have more work to do, we are excited about the progress we are making. With new apis like PerformanceObserver and long tasks supported by the browser itself, developers can use JS native apis to measure the performance of real users without degrading the user experience.
The most important metrics are those that represent real user experience, and we want developers to make it as easy as possible to satisfy users and create great applications.
The original address