Best Practices: Performance and Web Vitals

We at Viafoura take performance seriously, and we have put a lot of effort into making our systems speedy and responsive. For our client-side code, we are working diligently to minimize the impact that we make on our customers' applications.

There are some things that you can do to help in this area as well, some of which are outlined in the below article.

This article is a living document, and we will continue to add new information to this page, as we continue to improve our experience both for developers and end-users.

What can I do to minimize the impact of Viafoura's tools on my application's Core Web Vitals scores?

It's no secret that adding third-party JavaScript to your app will have an impact on the page load time, but there are things that you can do to minimize that impact, which we'll try to outline below.

To start - Core Web Vitals are split up into three categories:

  • CLS (Cumulative Layout Shift)
  • FID (First Input Delay)
  • LCP (Largest Contentful Paint)

Each of these metrics is measured individually as part of your page load sequence, and Google will use the score of each of these metrics when calculating the search index for your page. In short - your Core Web Vitals scores affect where your site will show up in search results. We will try to give a bit of information about each metric here, but we recommend reading content from Google on the topic for a deeper understanding. For a list of tools you can use to test your own pages, check out Google's article on the topic (we actually use Lighthouse a fair bit here on our engineering team).

CLS

The first metric, CLS, refers to how much the elements on your page shift during a page load sequence. Many modern web applications load various pieces of content asynchronously, with each request resolving at different times. As elements are drawn on to the page, they may appear in any order, causing the page layout to shift around as new elements are drawn. Some examples of this may be:

  • The main body text may render, but then a navigation menu at the top of the page loads, pushing the main body text down by X number of pixels (where X is the height of the navigation element)
  • A large block of text may render (imagine an article page), but then an ad placed after paragraph 2 loads, causing the rest of the article to shift down by Y number of pixels (where Y is the height of the ad element)
  • A side navigation container may load with some elements, then once the application knows the user's role/permissions, it may render more nav elements specific to that role/permission set, causing the rest of the elements to shift up or down

🚧

NOTE

Beyond CLS being a core web vital metric, it's also something that can greatly improve the overall user experience in your app, ensuring that the page's interactions are smooth, natural, and free of lag or β€œjank.”

There is a rule of thumb when it comes to loading asynchronous scripts that may render new elements on a page: set fixed minimum height values on the parent elements wherein these new elements will render. If you know what the final height will be, then setting that as the min-height on the parent element can eliminate the unsightly shift that happens when the script loads and the new element is rendered.

Our tools technically can cause layout shifts in your applications, but you can control how much or how little the shift ends up being. For example, if you are loading our Trending Conversations product, then you can set the min-height on the parent element that it will be rendered in to.

Given the below setup (where the parent element has a class of viafoura--content-recirculation)

<div class="viafoura viafoura--content-recirculation">
    <vf-content-recirculation />
</div>

the following CSS rule can ensure that the container is the same height as what will be rendered in to it later

.viafoura.viafoura--content-recirculation {
    min-height: 335px;
}

Several of our products have a set default height that you can add rules for on your side to help stop our scripts from affecting your CLS scores. For the ones that don't, we recommend at least setting some kind of min-height, which can minimize (not eliminate) the impact on the overall CLS score.

🚧

NOTE

These values will only work if you have not changed the default styles that come along with our products. If you've done any customizations, then you'll have to calculate the rendered height as per your customizations instead.

Default min-height values

The following list outlines either the default min-height, or some suggestions of how to handle setting default min-heights for our products. It is not exhaustive, however, as some of our products have different modes (eg. floating notification bell) or depend on the content rendering within (eg. standalone ads). There is also the fact that you can add your own styles to alter the look and feel of our products, including basic margin/padding rules in your application, rendering these values moot.

NOTE: The following values are meant as a guideline, but may not be 100% accurate. _We strongly recommend that you setup your implementation with all the features you want, then inspect the rendered height of each product in your browser once it's up and running. You may find that your application's CSS increases the rendered height of our elements.

ProductDefault / notes on heights
vf-content-recirculation (horizontal)282px
vf-content-recirculation (vertical)depends on the value of limit - each row is 60px tall, so limit 5 = 60*5 = 300px
vf-conversationssee the 'initial-height' prop for details on how to customize this
vf-live-chatsee implementation docs for more details, as height is required for this product
vf-live-blogthe height varies drastically, but we recommend a min-height of at least 500px
vf-conversations-countdepends on your root font-size
sharebar (horizontal)65px
sharebar (vertical)375px

FID and LCP

Both of these metrics are related to speed in two factors: time to download assets, and time to execute code.

The time it takes to download our bundle will depend on a number of things:

  • The end-user's connection speed (eg. an old phone on a 3G network vs. a new laptop on a wired ethernet connection)
  • The number of other scripts that your page requests to load
  • The amount of memory available on the end-users device (eg. loading a website while your computer is running a 60fps game could be a touch slower than loading a website while your computer has no other applications running)

All of these things are outside of our control, but there is a relatively simple technique that you can do to minimize the impact of our script on your pages' loading sequences. The following technique could technically be used for any scripts that your application may download, but we recommend that you try each script one-at-a-time to ensure you don't break any other functionality.

Deferred loading of third-party scripts

πŸ“˜

We already do this for you!

Note that the Viafoura Entry Point script already does this for you. This is here as an example of what you could do to further improve performance on your pages.

One way of minimizing the impact of third-party scripts when it comes to speed metrics is to add a mechanism into your application to defer loading our JavaScript bundle until it's absolutely necessary. A common way to do this is to use an IntersectionObserver to detect when an element that will render a third-party component enters (or nears) the viewport, as the user scrolls through the page.

A basic example of how to do this follows, but the IntersectionObserver API is vast, with all sorts of fine-tuning you can do when it comes to thresholds. We recommend checking out the MDN page for a deeper understanding if you want to get more fancy.

The following example will load the a third-party script the moment that the #third-party-container element enters the viewport.

function deferLodingThirdPartyScript(untilSelector: string): void {
    const untilElement = document.querySelector(untilSelector);
    if (!untilElement) {
        return;
    }
    const observer = new IntersectionObserver((entries, observer) => {
        entries.forEach(({target, isIntersecting}) => {
            if (target === untilElement && isIntersecting) {
                loadThirdPartyScript();
                observer.unobserve(target);
            }
        });
    });
    observer.observe(untilElement);
}

function loadThirdPartyScript(): void {
    const head = document.getElementsByTagName('head')[0];
    const script = document.createElement('script');
    script.setAttribute('src', 'https://cdn.third-party.com/third-party-script.js');
    script.setAttribute('async', 'true');
    head.appendChild(script);
}

// Somewhere in your application's initialization
deferLoadingThirdPartyScript('#third-party-container');

🚧

NOTE

The IntersectionObersver API is available in all modern browsers, but there are older desktop and mobile browsers where it cannot be used. In these cases, you could choose to load a polyfill, such as this one provided by the W3C.

Performance at Viafoura

We are constantly striving to make our products better, and performance is one area where we are putting a lot of attention these days. This particular guide is a working document, and as we continue to find ways to make our products better, we will add them here.

Our roadmap for performance currently includes 3 main buckets:

  1. Minimizing page load impact by making our entry point much, much smaller

    Our current bundle is larger than we'd like it to be, so we're going to make it smaller for you, ensuring that your app is not blocked by our scripts at page load time.

  2. Optimize chunks that are loaded per product

    A lot of our code is bundled together in one big payload, so we're going to break it apart into smaller bundles, ensuring that your users only download exactly what they need.

  3. Integrate performance testing into our pipelines

    If changes being introduced affect our performance too much, then we won't release them!