Performance & the Fullstory Script

The Short Answer:

Fullstory will have no discernible effect on website performance.

The Technical Details:

The Fullstory data capture script (fs.js) is carefully constructed and measured to ensure that it has negligible impact on the performance of any page on which it is embedded. To understand the potential points of impact and the steps we have taken to ensure maximal responsiveness, we have to consider the following interactions: page rendering and interactivity, ui thread, and upload bandwidth. Further, it is critically important to avoid any errors in the client page, so we must also consider DOM and script interactions.

Page Rendering and Interactivity

First and foremost, we will not allow the script to delay rendering or interactivity of your page. To this end, we employ the following techniques:

  • Keep the script small

    Fullstory serves fs.js with both Brotli and gzip compression. The file is typically around 80 KB with Brotli, or about 95 KB with gzip—smaller than most scripts and all but the smallest images. Size changes over time as features ship and optimizations land; see the fs.js changelog for the current size.

  • Reduce fetch time

    The script is edge-cached on a CDN at edge.fullstory.com and typically takes about 20ms to fetch (24ms in the example below). This is particularly important because browsers will only allow a fixed number of outstanding resource requests at any given point in time. The faster this request completes, the faster it frees up slots for other resources.

  • Get out of the way

    JavaScript’s semantics require that an inline script of the form <script src='fs.js'> block evaluation of the page until the script is loaded, which could have a noticeable impact on page load time. We sidestep this risk by installing a very small script stub that loads the full fs.js script asynchronously, guaranteeing that it’s requested after any static resources such as CSS and images. Note that this is the same strategy as that employed by Google Analytics and similar products. The following is a network-load graph of a typical session. Note that fs.js is requested asynchronously from the page (81.5 kB in 24ms) and loads in flight with other resources rather than as the last request:

fs.js shown in the Chrome Developer Tools Network tab.

Bottom line: The Fullstory script will have no discernible effect on your perceivable page rendering time, and its unavailability will not impact your page.

UI Thread Utilization

After a page is loaded, the browser’s most precious resource is CPU time spent in the main UI thread. All Javascript is executed on this thread, and if it blocks for more than 50-100ms, it can result in user-visible "hangs." We have taken careful steps to optimize the Fullstory script to avoid this.

Event Handlers

In order to capture all interactions on the page, fs.js captures most common event types (e.g., mouse, keyboard, resize, DOM mutations, and so forth) at the top level. To avoid interfering with the page, these event handlers must do the minimum possible work and return control to the browser’s event loop. To this end, they all simply add events to an in-memory queue to be processed later. To get a sense for what this means in practice, see the following Chrome timeline graph:

Function call triggered by mousemove event with a duration of 19µs

Here we see a series of pointermove and mousemove events being handled by the data capture script. The relevant value here is Duration in the right panel, which is 0.019ms (19µs)—at least three orders of magnitude below anything a human can perceive.

Outgoing Event-Queue Processing

When it’s time to upload a set of events to the server, the data capture script has to process the outgoing event queue, which can of course be a bit more expensive than just enqueueing an event. For the vast majority of cases, however, this takes very little time. Here’s a typical case:

sendEvents in the event stack, with a duration of 0.11ms

Here we see one timer callback that runs a function call on /s/fs.js, then t.processEvents, and ends in t.sendEvents. That sendEvents call takes 0.11ms—still far below a user-visible hang.

Compression and Upload

There are cases that can take a bit longer to process. This typically occurs because of large DOM mutations that must be compressed before being uploaded (more on the compression process below). But it’s notable that in these cases the data capture script does significantly less work than the script that produced the mutation in the first place — e.g., an existing 100ms event becomes a 110ms event. These large mutations are almost always associated with significant user actions such as paging a table or list view, where a few extra milliseconds is unnoticeable.

Upload Bandwidth

While it tends not to obviously create a user-visible performance impact, it is important that we not push more data upstream than strictly necessary. The data capture script captures the entire DOM, which can become fairly large (though not larger than the size of the downloaded HTML). To mitigate this, the script gzip-compresses all HTML in the browser—using the built-in CompressionStream API—before it’s sent to the server, and flags the encoding so the server can decompress it. This typically gives a compression ratio of 40-60%.

Technical note: We compress the upload ourselves rather than relying on the browser’s automatic HTTP compression because, due to historical problems with the design of HTTP 1.1, almost no browser will gzip a request body when uploading data.

The script also does not send externally-fetched CSS or images over the network, which often form the bulk of a page’s data. These are instead fetched later by the Fullstory servers.

Server Load

Because Fullstory servers fetch external CSS and images just after a page is captured, it must load these resources directly from the application’s server. In order to minimize these extra requests, Fullstory employs two levels of caching. The first is a standard HTTP client cache, which both respects standard cache headers and includes rate-limiting to ensure that it doesn’t fetch too frequently even if the cache headers are incorrect. The second tier shares fetched resources across all sessions for a given customer. Practically speaking, this means that your servers will see very few extra requests.

DOM and Script Interaction

Modern installs use version 2.1 of the Fullstory snippet, which automatically calls FS('init') with your orgId, host, and script. Additional capture settings belong in that init call—for example FS('init', { env: { … } }) with options such as captureOnStartup or cookieDomain. Settings passed after capture has started are rejected. See Configure Capture for the full list of options.

Legacy window._fs_* globals are still read once at startup and then overlaid by init (init wins), so existing installs keep working. New installs should not add these globals.

On the page, fs.js exposes its API on a single global— window.FS by default, or a custom name set via the snippet's namespace argument or the data-fs-namespace attribute. To prevent a second agent from capturing the same page, it sets _fs_loaded on that namespace object (for example FS._fs_loaded), and it writes _fs_shutdown on window.

The script is careful to ensure that none of its event listeners are attached in such a way that they are visible to other scripts on the page. It also tracks captured DOM nodes in an internal map rather than modifying them, so it does not add any properties to the nodes on your page.

Ongoing optimization

Performance work on fs.js is continuous, and it pulls in two directions at once: we keep shipping new capture capabilities while also reducing the script's download size and CPU cost. Because those goals work against each other, the compressed size of fs.js moves within a band over time rather than trending steadily in one direction. Every change is published on the fs.js changelog, which lists the current size.


Was this article helpful?

Got Questions?

Get in touch with a Fullstory rep, ask the community or check out our developer documentation.