Web performance investigative techniques

Eugenia Zigisova
9 min readJan 6, 2021

I am working in a team that inherited a big legacy application that sometimes reminds me of Howl’s moving castle from the Miyazaki movie. Similar to the castle, it consists of patched together code from many different technologies (WordPress, PHP, Angular, Vue and React), and like the junked together castle, its page speed is quite slow. While this structure makes for good movies, it does not make for a good application. So, to fix the problem on a fundamental level, a front-end re-architecture strategy was created. However, despite our desire for a sleeker and faster application, it is not easy or fast to implement.

In general re-architecture or refactoring is a compromise between business priorities and technical improvements. We can’t just redirect all developer resources to rebuilding a website when there might be more important business goals. Which, to be honest, is fair because that is what keeps the company alive and brings money. But what we can do is make performance improvements that don’t require so much effort as re-architecture, and yet deliver a business value by improving user experience.

Even if re-building a legacy website isn’t on your company’s radar, it is important to work on performance right now. Replacing a legacy website isn’t as quick as a wink. And bear in mind an old app will be used for quite a long time until it is completely replaced.

This article is focused on how to analyse a website to find performance issues along with their solutions.

See the issues 👀

As any successful project should have measures of success, so do performance improvements. It is vital to collect data at all steps even if the improvement is gradual, or non-existent at some steps. As all your teammates are using powerful laptops and have a good internet connection, performance issues might not be apparent to them. Collecting metrics helps to raise awareness in this scenario. It also helps building a performance culture which is “the hardest thing to achieve” as stated by the Telegraph engineering team.

For example, you could run a smoke test with the Lighthouse tool. It gives the most critical performance metrics like First contentful paint, Largest contentful pain, First Input Delay. Look at the number of users by device type and OS in Google Analytics or another tracking tool to understand how many users might experience the app loading slowness. Most of the tracking tools provide you with insights into page loading time based on real user data that might differ from the lab data you collect on your machine.

Additionally, numbers are your leverage when negotiating on performance improvements with product managers and stakeholders.

Investigate 🕵️‍♀️

Bad performance is like a crime and as such you should imagine yourself as a detective on the case. Your job is to catch JavaScript tasks that are blocking rendering. You should find the synchronously loaded scripts and the most heavy sources. Detecting redundant CSS and images that aren’t lazy-loaded is your task too.

Performance smoke test

Usually, the aforementioned “criminals” leave some evidence to be found in the browser Dev tools. However, a Lighthouse tool is also available there. It is useful not only because it shows you web vitals, but it also suggests improvement ideas. For each opportunity, you will get a list of sources, i.e. JavaScript or CSS files you can optimize. Which makes for easier detective work!

You may notice an option to check 3rd-party resources. Generally, it is nice to delegate some website parts to the expert service providers except when they slow down the page. For instance, User Centrics — a service we use for GDPR-compliant consent management, has large JavaScript assets that were synchronously loaded blocking the page rendering. Unfortunately, we can’t make it asynchronous because the page has important parts that can not be rendered until the user has given their consent with help of User Centrics widget. However, we can preload it, saving some loading time.

In order to measure the impact of third-party scripts, do the following:

  1. Measure the current loading time using the Network panel. It shows you how much time it took to load the page at the very bottom below the list of all requests.
  2. Block the URL of the 3rd-party source.
  3. Clear the Network tab and record the page loading again.

☝️ It is recommended to run the same tests several times in order to get more precise data.

Javascript bundle size

With the Network panel, you can explore resources that take the longest time to load. Don’t forget to disable cache to simulate the initial page load.

Do you see that huge “vendor.js” bundle that weighs 704KB and takes 166 ms to download? Unused JavaScript slowing down the page might be lurking there! Webpack bundle analyzer can shed light on which libraries are inside the bundle.

Make a round of interrogation with each alleged JavaScript library asking for an alibi: Why are you in the bundle? What were you doing until the Largest Contentful Paint? 🤨

If the library was not used during the page loading, enable dynamically import, so it would be automatically split from the main bundle by Webpack and not requested on the first page load.

Be suspicious about the large size of a package. Consider direct imports of the library methods instead of importing the whole library. For example, by cherry-picking “lodash” methods only a subset of the library code will be added to the bundle.

Or you can explore more lightweight libraries. For instance, if you are using “moment.js” consider recommended alternatives.

☝️ It is suggested to deliver less than 170KB of JavaScript, HTML, CSS, images, and other sources to ensure the website loads fast on low-end devices and slow network.

Long tasks

Every process performed by the browser in order to display the final web page, i.e. parsing HTML, executing scripts and microtasks, rendering, etc., is done on a single thread that can do only one process at a time. Hence, it is important to avoid long JavaScript tasks that block the main thread and postpone the rendering of the page. But what does that mean — a long task?

To ensure a smooth and interactive UI, the web page should be able to render 60 frames per second or one frame per 16 milliseconds. Namely, there are only 16 ms between each frame to perform JavaScript tasks without causing UI glitching.

☝️ When a JavaScript task takes more than 16 ms, it is considered as render-blocking and can cause UI glitching.

However, in W3C docs it is stated

A long task is an event loop task including associated microtasks whose duration exceeds 50ms.

The less time it takes — the better.

Analyzing performance bottlenecks and long running tasks

A script might become a performance bottleneck when requested synchronously in the head of the HTML page, thus, stopping the process of constructing the DOM until the script execution is finished.

Dev tools Performance panel is your partner in investigating performance bottlenecks and long tasks. It should be used in Incognito mode to not be thrown off the scent by the browser extensions. In order to understand which tasks are executed during the page load, start recording the performance, and refresh the page. To simulate the most dramatic scenario choose the “3G slow” Network and “6x slowdown” CPU in the settings. We will take a deeper look at the FPS, CPU, and Main section.

FPS and CPU

The FPS (Frames per Second) chart presents the framerate marking in red the parts where it is low enough to harm the user experience.

The CPU chart displays the overview of CPU usage by type of task. The yellow color represents JavaScript tasks.

☝ ️The parts where FPS is red and the CPU is high and yellow require special attention and broader analysis.

You can zoom in a section on the FPS/CPU chart and see a detailed view of the processes happening during a selected period of time.

Main thread

The main section displays a flame chart of the activities in the main thread. If it is a long task, there will be a red triangle in the right corner that is a signal to take an even deeper look at the task.

The call tree in the bottom panel shows all the functions triggered during the task. You might think it is impossible to trace the source as there are plenty of anonymous functions but if you go down the tree you will finally see familiar function names.

By using the “control + F” shortcut it is possible to find function calls you are curious or suspicious about. For example, in the project I am working on, there is a registration page. There are different registration flows that are based on user and distribution partner parameters that are initiated in the Redux store. The initiation happens as a chain of promises as follows:

store  .dispatch(initiateRegflow())  .then(() => store.dispatch(initiateUser(...)))  .then(() => store.dispatch(initiatePartner(...)))  .then(({ ... }) => {    // ...    store.dispatch(      updateSettings({        loading: false,      }),    );});

The registration form is not rendered until all parameter initiation finishes. Unsurprisingly, the Largest Contentful Paint takes from 3 to 13 seconds on the registration page depending on the device. I was guessing if the parameter initiation is the cause and decided to analyze the registration loading using the Performance panel. After searching for initiateRegflow , initiateUser and initiatePartner the following picture unfolds.

Although actual function execution takes a tiny amount of time, the functions won't be triggered one after another straight away. Instead, they will “wait” for others including 3rd-party originated tasks to complete. The time frame between initiateRegflow and initiatePartner can reach up to 6 seconds (!) on the slow network and low-end device, i.e. registration page loading takes 6 seconds more than it theoretically could.

Since promises are microtasks they are executed only after a task is completed and in the sequence of the job queue. To learn more about it, I recommend Jake Archibald's “Tasks, microtasks, queues, and schedules” article that helped me to understand the JavaScript concurrency model better.

To sum up, long tasks are not the only reason for render-blocking JavaScript. Sometimes, due to JavaScript asynchrony, the execution order is not straightforward and might affect user-perceived web page performance.

In short

As a web developer, you are equipped with various tools for web performance analysis. Different developer tools solve different issues and here is the summary of how to use them:

Lighthouse

  • Collect key performance metrics,
  • Quickly identify major performance problems,
  • Get a list of improvement ideas.

Network tab

  • Identify most heavy script and CSS files,
  • Measure the impact of third-party scripts by blocking their source URLs.

Performance tab

  • Identify bottlenecks that block page rendering,
  • Find long-running JavaScript tasks,
  • Analyze JavaScript task execution order.

--

--