Refactoring a Web Performance Snippet for Security and Best Practice
In this blog post, I'm going to discuss a little Web Performance Snippet that I've seen a few WebPerf evangelists use on their websites. As you will have seen in a previous blog post, I've recently overhauled my blog, both in terms of design and also static hosting. In doing so, I've completely rewritten almost everything from my old site by either migrating it across and "cleaning it up", or simply realising the feature was no longer useful and discarding it, thus reducing technical debt in order to streamline maintenance in the future.
Security
In my last blog post, I touched on improving the security of this blog. And this is the reason for this post (in a very roundabout way!). For the past few years, since I started strengthening my Web Performance knowledge, I've had a little code snippet in my footer. It simply queries the Performance API and outputs an elementary Page Load time via the use of loadEventEnd property. Unfortunately, these are now deprecated features of the PerformanceTiming API. But as they still work, I've had no reason to change it. I'd love to say I came up with this idea and code myself, but I didn't. I actually copied this feature from Tim Kadlec several years ago, it can still be seen in the footer of his website today (bottom-right corner of his website):

Examining the code that outputs this paragraph to the page, I can see it looks like this:
window.onload = function() {
setTimeout(function() {
window.performance = window.performance || window.mozPerformance || window.msPerformance || window.webkitPerformance || {};
var t = performance.timing || {};
if (!t) {
return;
}
var start = t.navigationStart,
end = t.loadEventEnd
loadTime = (end - start) / 1000;
var copy = document.querySelectorAll('.copy');
copy[0].innerHTML += "<p class='loaded'>This page loaded in <strong>" + loadTime + " seconds</strong>.</p>";
}, 0);
}And this is the code I had running for many years on my old website. I liked having this code on my website as it gave me an extremely basic statistic on how well the site loaded on any given device I happen to use (e.g. an older mobile device, or when I'm using an unstable connection in a rural area of the country). It could also be used to verify there's no obvious performance problems. For example, if I see that the metric is anywhere over 500 ms (on my laptop) then that's a good indication that something isn't performing as it should be. Over 500 ms for such a simple static page on an 2020 MacBook Air (M1) would be a real worry!
Updating the code
Unfortunately, there are a few issues with this code that wouldn't work with my newly updated website. The biggest of all being the use of the setTimeout() which is a type of eval() and is a huge security risk. You only need to look at the MDN Page for eval, and it comes with a big red warning at the top of the page:

They even have a whole section on the page as to why you shouldn't do this.
For my newly built blog, I planned to update my blog to use a fairly strict Content Security Policy (CSP). And as I intended to use the unsafe-inline CSP directive AND didn't want to go down the Nonce-based strict CSP or Hash-based strict CSP route due to the additional complexity it adds to the site. For these reasons, and also for something to do over the Christmas Holiday period, I thought I'd just refactor the code and use something more modern, like the PerformanceObserver API.
In doing so, I'd remove the need for the setTimeout code:
setTimeout(function() {}, 0);The above code uses something called "macro-task scheduling". This is where the browser adds this function to the end of the event loop queue. In doing so, it ensures:
- The load event handler has finished.
- The browser has had time to populate all the performance timing values.
- The
performance.timing.loadEventEndproperty has a valid value.
Without this macro-task scheduling code, the loadEventEnd value won't have populated by the time it is required, which would likely result in a Not-A-Number (NaN) value being displayed on the page (or alternatively a non-helpful value of 0).
Refactored Code
In the updated code below I refactored it by:
- using the Optional chaining operator for detecting
window.performanceandwindow.performance.getEntriesByType. - removing the setTimeout "macro-task scheduling" code for greater security (no more eval!).
- using a modern API for optimal code execution (
PerformanceObserverAPI). - Wrapping the code in an Immediately Invoked Function Expression (IIFE) to ensure the browsers global scope isn't polluted with random functions and variables.
- using Template literals (Template strings) for concatenating the final string that is injected into the DOM.
- replacing the deprecated PerformanceTiming API and have migrated to the PerformanceNavigationTiming API.
<!-- HTML in my footer -->
<footer>
<script src="/js/webperf.js" defer></script>
</footer>// webperf.js
(function() {
/* check to see if the browser is modern enough to support the
PerformanceNavigationTiming API */
// use "optional chaining operator" modern replacement for
// (if (!window.performance || !window.performance.getEntriesByType))
if (!window.performance?.getEntriesByType) {
console.warn('Performance API not supported');
return;
}
const observer = new PerformanceObserver((list) => {
const entries = list.getEntriesByType('navigation');
const pageLoadTime = entries[0].duration;
// Create the paragraph element dynamically if it doesn't exist
let loadTimeElement = document.getElementById('pl');
if (!loadTimeElement) {
loadTimeElement = document.createElement('p');
loadTimeElement.id = 'pl';
// Find the footer element and insert the paragraph as the first child
const footer = document.querySelector('footer');
if (footer) {
footer.insertBefore(loadTimeElement, footer.firstChild);
}
}
loadTimeElement.textContent = `This page loaded in: ${pageLoadTime} milliseconds.`;
});
// buffered: true - The observer will also receive entries that occurred before it started observing
observer.observe({
type: 'navigation',
buffered: true
});
})();In refactoring Tim's code, I deliberately chose to exclude certain older browsers and their users from accessing this feature. The refactored code includes a check for the PerformanceNavigationTiming API, ensuring the web performance snippet is only added to the footer if the browser supports this feature. This prevents older browsers from displaying a partially broken footer. Interestingly, this approach aligns with a modern take on a methodology Tim already uses on his website. His website follows a progressive enhancement technique known as "cutting the mustard", a concept introduced by the BBC News web development team in April 2013!
Cutting the mustard is a technique that involves categorising browsers into two groups: older "feature browsers" with limited capabilities and modern "smart browsers" that support advanced features. By adopting a two-tiered approach to responsive web design, a core experience is delivered to all users, while additional enhancements are applied for more capable browsers. This ensures broad accessibility while providing a refined and feature-rich experience for modern devices like smartphones and larger screens. I actually used my own version of this in a previous blog post back in November 2019, when I implemented Webmentions purely in a client-side JavaScript in my blog post: "Implementing Webmentions on this blog". TL;DR: Gist here.
Thankfully, most modern browsers now auto-update every 6-weeks without users even knowing, so these older browsers are becoming less and less of an "issue".
For example, let's have a look at the refactored features I mentioned above:
PerformanceObserver API
This API allows developers to asynchronously observe and collect performance-related metrics, such as resource loading times, navigation events, and custom user timing marks. It listens for entries in the Performance Timeline, filtering them by type (e.g., resource, navigation, paint) and executes a callback function whenever new entries are available. This API is useful for real-time performance monitoring (RUM) and analytics, e.g. Google Analytics.
The PerformanceObserver API was introduced into most major browsers between 2016 and 2017, Microsoft Edge was the last to introduce the API in January 2020 when they migrated over to using the Chromium browser engine, rather than EdgeHTML.
According to Can I Use, 96.64% of users globally now use a browser that supports the PerformanceObserver API.
Template Literals
Template literals in JavaScript are string literals enclosed by backticks (`) that allow for embedded expressions using the ${expression} syntax. They support multi-line strings, string interpolation, and special constructs like tagged templates for advanced processing of literals.
Template Literals were introduced into the JavaScript Language in ECMAScript 6 (ES6) which was "released" in 2015. Amazingly, most major browsers managed to implement the new specifications in the same year (2015), including iOS Safari 9.2 AND Edge 13 (EdgeHTML).
According to Can I Use, 97.97% of users globally now use a browser that supports the Template Literals language feature.
PerformanceTiming API
This API is part of the deprecated Navigation Timing API, that provides detailed timestamps for key events in the navigation and page load process, such as DNS lookup, server response, DOM processing, and resource loading. Accessible via performance.timing, it allows developers to measure metrics like page load time, Time to First Byte (TTFB), and DOM readiness. While useful for diagnosing performance bottlenecks, it has been largely replaced by the more precise and structured PerformanceNavigationTiming API, available through the performance.getEntriesByType('navigation') method.
The PerformanceTiming API is now deprecated, but it was first supported by all major browsers back in 2015. Incredibly, IE9 even supported it way back in 2011!
It's not recommended that the API be used today because at some point browser vendors may consider removing it. According to Can I Use, 97.38% of users globally now use a browser that supports the PerformanceTiming API.
PerformanceNavigationTiming API
The API is a modern, high-precision replacement for the deprecated PerformanceTiming API, offering detailed metrics on navigation and page load performance. Accessible via performance.getEntriesByType('navigation'), it provides timestamps for events like DNS lookup, server response, DOM processing, and resource loading, along with new attributes like transferSize and decodedBodySize for enhanced analysis. It simplifies performance monitoring with a single object and is ideal for modern web applications, including Single Page Apps (SPA's), making it the preferred choice for navigation performance insights.
If you know you are still using the PerformanceTiming API, it is recommended that you update to use the PerformanceNavigationTiming API. Although it's unlikely that support for the older API will be fully removed, the new API offers so much more functionality and is a much easier API to use. You may as well take advantage of the new features and functionality that is built into all modern browsers.
According to Can I Use, 96.06% of users globally now use a browser that supports the PerformanceNavigationTiming API. It was supported all the way back in Edge 12 (EdgeHTML), and was finally supported in iOS Safari in version 15.3 back in December 2021.
Data Driven Decision-Making
So when it comes to browser support for modern features like this, how is it best to approach the old browser issue? In my opinion, you should approach it as a data collection exercise. If you have data to show that your site has browsers visiting that are approaching a decade old (at the time of writing), you should absolutely continue to "cut the mustard" to support those users.
Although I believe there's a very valid counterargument too. That by supporting such old and outdated browsers you're actually harming a users' experience of the web as a whole, by allowing them to use such an outdated browser. Also, can you imagine how vulnerable a user who is still using a browser like IE9 would be on the modern web? Finally point, the web performance barrier to entry of modern JavaScript-driven websites would make the internet completely unusable for these browsers! With a Median JavaScript size of 650 KB(!) as of December 2024, even a modern desktop browser would struggle!
Summary
So there we go, another blog post related to updating nooshu.com to 11ty and the steps I took to shed the years of technical debt I'd accumulated and move to a more maintainable and forward facing blog. Big thanks to Tim Kadlec for writing the original code snippet and inspiring me to add it to my blog! As always, I hope you enjoyed reading my random ramblings, and please do let me know if you have any comments or feedback.
Post changelog:
- 02/01/25: Initial post published.