perf 2.png

Optimizing web pages is pretty challenging. Except in the simplest cases, it requires a lot of manual work to understand what is happening on the page and identify areas for improvement. The basic reason for these difficulties is the limitations of existing tools, which are descriptive but not explanatory. These tools can show all the events that happened while loading a page, but this is both less and more information than needed. Less in that the tools can’t say why an event happened or what effect it has on the page’s performance, and more in that events that are irrelevant to improving performance are mixed in with events that are critically important.

We propose a new and more explanatory abstraction, a limiting path, which lists the events that must occur in sequence before a web page loads or responds to user input. The only way to speed up the page is to optimize the limiting path; other unrelated events can be ignored. Replay’s upcoming performance analysis is centered on limiting paths: it computes these paths for any event of interest in a recording of a page’s execution, and analyzes how time was spent along the path. This makes it easy both to optimize the page and to precisely identify performance regressions, including in CI.

Untitled

While these techniques apply to any web page, our focus is on React applications, which have a lot of implicit and asynchronous behavior that is difficult to understand and optimize.

In the rest of this post we’ll describe a real world regression in Replay’s devtools we investigated, the limitations of existing tooling investigating this regression, and how limiting paths highlight the problem immediately. Our main focus is on network regressions, but towards the end we also discuss overhead due to main thread computation.

Replay’s performance analysis will enter closed beta this summer. Join our waitlist here to get access when it’s available.

The Regression

A recent change to Replay’s devtools added the logic below to the App function which is used to render one of the top level React components in the devtools.

const App = ({ apiKey }) => {
  // ... initialization ...

  const [token, setToken] = useState(apiKey ? { token: apiKey } : undefined);
  useEffect(() => {
    async function fetchToken() {
      const response = await fetch("/api/token");
      const token = response.ok ? await response.text() : undefined;
      setToken({ token });
    }
    if (!token) {
      fetchToken();
    }
  }, [token, setToken]);

  if (!token) {
    return;
  }

  // ... continue rendering page components ...
};

This change does not impact the rendering flow when the user is already logged in, but when the user is not logged in it has the effect of creating an extra network call that blocks the page from loading.

When the user is not logged in the apiKey prop will be null, and the first time the App component renders an early return is taken because no token is available. The useEffect hook runs after the first render, triggers a fetch on /api/token, and when that fetch returns the token will be set. This triggers a rerender of the component with the new token and the page load resumes.

Waterfall Woes

The main tool developers have to investigate network related performance issues is a network waterfall chart, which is available in Chrome’s devtools as well as other performance tools. Below is a waterfall for loading Replay’s devtools with the above regression present.

https://lh7-us.googleusercontent.com/Upxs2TqcliblwGLw9eCy6bEuV6g0esJFm_qx2HwQoA4SG7JcpXILmzbplisIgXpey1Fa416A_hJqBCpQHVXfMCSSy0GQMiQwrnrxqBx39Z2qNqLEtvu4kYY0tsuiTS_6oDi1o-bJBKE5abt0bT5YoW0

Each row in this chart is a network request, with columns for the request information and the waterfall itself on the right to show timings for when each request was initiated and later completed.