Author: Ben Bozzay
Website performance measurement should occur locally in your dev environment. Browser development tools and JavaScript APIs will enable quick iteration without needing to rely on third-party websites or audit reports.
The specific concepts and metrics will be explained in detail in the relevant Core Concepts modules. This section provides a general overview.
We'll primarily use browser Dev Tools that speed up local development. On Windows Google Chrome, the hotkeys top open dev tools are ctrl + shift + i
or command + shift + i
on mac.
When working in a local dev environment, network requests don't have latency that would be seen in a production environment (on a server). Additionally, your specific internet connection or workstation's hardware might not reflect the average user.
In the network tab, select the Online dropdown and click add.
Create a profile with a slower download speed and simulate network latency.
When testing for performance, it's important to clear the browser cache when reloading. Otherwise, the browser will likely serve cached files, which won't provide an accurate picture of what the initial load speed is like.
It's easy to clear the page cache and refresh with CTRL + SHIFT + R
or CMD + SHIFT + R
. However, you can also configure dev tools to clear cache on reload.
The "network" tab in dev tools is an easy way to check a few key metrics without running a profiler.
Files downloaded by the browser appear on a timeline, allowing us to see the download time and order of resources.
One of the first metrics we'll consistently check is how long it takes the HTML Parser to construct the DOM or Document
object. In the network tab in dev tools, the blue DOMContentLoaded
metric appears on the timeline at 124ms. This metric includes the download time of the HTML document + the HTML parser time.
After all resources are downloaded and evaluated, we get the load metric.
load
is equal to DOMContentLoaded
time + the download and evaluation time of all resources (including images).
The performance tab can be used to examine more metrics related to the visual rendering process and the activity on the main thread.
Clicking the reload button will refresh the page and record the performance.
The Performance timeline allows us see what the page looks like at specific points during the rendering process with screenshots overlayed on the timeline.
Note about the screenshot timeline: if you hover over the start of the timeline, you might see screenshots of a fully rendered page. This is because the profiler is showing the page from a previous profiling session.
The profiler uses the black line to show the start of the current profiling session.
Various metrics are annotated on the timeline. The order of these annotations on the timeline can vary depending on how the HTML is structured and optimized.
Document
object.The profiler provides information about tasks the browser is performing via the "main" row.
Task order and timing occur from left to right. The actions within each task are displayed in each row below the generic "task" heading.
Important: When running the profiler, some tasks run on the timeline passively.
It's useful to use named functions to more easily identify tasks you are testing for on the timeline.
JavaScript APIs can give us a more precise look at performance and also help us understand what goes into pagespeed metrics. The MutationObserver API can be used to track how long it takes for the HTML Parser to parse individual HTML nodes.
You don't need to know how these APIs work or have an in-depth understanding of JS. Implementing these are optional and it's not a requirement for pagespeed optimization.
We can console.log
every time the HTML parser discovers an element and attaches it to the DOM.
// Timestamp to compare to
const startTime = new Date();
// Timestamp for each node
var lastNodeTime = new Date();
// Initialize the observer
var observer = new MutationObserver(function(mutations){
for (var i=0; i < mutations.length; i++){
for (var j=0; j < mutations[i].addedNodes.length; j++){
// Pass the newly attached node to a callback function
checkNode(mutations[i].addedNodes[j]);
}
}
});
// Watch for changes to an element (like childNodes being added)
// Attach the observer to the root HTML node (<html>) via the first argument
// Pass options to the second argument, such as watching changes to nested nodes
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
let checkNode = function(addedNode) {
if (addedNode.outerHTML) {
// Create a formatted table with the node HTML, time difference, and timestamp.
console.table({
"Node HTML": addedNode.outerHTML,
// How long this node took to add to the DOM
"Time Since Last Node Added": new Date() - lastNodeTime,
"Added At": new Date() - startTime
})
// Reset timestamp
lastNodeTime = new Date();
}
}
// Optionally, log when DCL occurs (last childNode is added)
document.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded Event Fired in", new Date() - startTime)
observer.disconnect();
})
Using the PerformanceObserver API at the top of our HTML file, we can console.log
more specific details about network requests and general page performance.
let results = {};
var performance_observer = new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
// Display each reported measurement on console
console.log(entry)
results[entry.name] = {
"start": entry.startTime,
"duration": entry.duration
}
console.table(results)
})
});
performance_observer.observe({entryTypes: ['frame', 'navigation', 'resource', 'mark', 'measure']});
performance.mark('registered-observer');
Many of these metrics are displayed in devtools, but this gives us more control over our profiling.
With the previous MutationObserver
setup, we can even see how long JavaScript pauses DOM Construction by examining the timestamps that we setup.
Here we can see that the
delay()
function caused about a 100ms delay in DOM construction because of the "Time Since Last Node Added" metric that is console logged after it.
We can also use PerformanceObserver
to set timestamp markers that allow us to benchmark specific parts of our code.
function clicked(elem) {
console.log("Clicked")
performance.mark('click-function-start');
// This function takes 3 seconds to run
delay(3000)
performance.mark('click-function-end');
performance.measure('delay', 'click-function-start', 'click-function-end');
}
In this case, the
clicked()
function called by the click event takes 3 seconds to finish.