Lately I've been putting together my portfolio for frontend roles that incorporate web performance and user experience. While working on my Personal Site, I discovered a neat tool called Speedlify, an open source repository to help automate continuous performance measurements created by @zachleat.

Screenshot of rendered Speedlify web component

You can easily integrate Speedlify into your website by importing it as a native Web Component.

    *.html    
<!-- "https://github.com/zachleat/speedlify-score" -->
<speedlify-score speedlify-url="https://www.speedlify.dev/" hash="bbfa43c1">

While the Speedlify component is primarily designed for 11ty sites, I've incorporated it into my own website, which utilizes Vite as the build tool and SolidJS as the library.

This is the process I followed if you wish to do the same.

Requirements

See @zachleat's article for instructions to setup the speedlify-score web component for 11ty sites. I'll be avoiding redundant information here.

Speedlify's architecture is pretty straightforward. I've set up IFTT to trigger a Netlify build on my deployed Speedlify instance to update site performance scores daily.

Diagram for Speedlify component

Perf Considerations

  • Non-blocking Main Thread: The Speedlify component fetch request is managed asynchronously to avoid blocking the main thread, ensuring a smooth user experience. This isn't a critical view component so we can defer the work after the document has been fully parsed and all resources have loaded.

  • Allocating Render Space: Space is pre-allocated for the Speedlify component to prevent layout shifts during rendering, providing a stable and predictable user interface.

  • Data Caching: A caching mechanism is used to store fetched data, reducing network requests and speeding up component loading, thereby enhancing site efficiency.

Type Definition

Add this to your type definitions file to remove the Property 'speedlify-score' does not exist on type 'JSX.IntrinsicElements'.ts(2339) error:

    *.ts    
/* types.d.ts */
declare module 'speedlify-score'

interface SpeedlifyScoreProps {
  'speedlify-url': string
  hash: string
  score: boolean
  weight: boolean
}

declare global {
  declare module 'solid-js' {
    namespace JSX {
      interface IntrinsicElements {
        'speedlify-score': SpeedlifyScoreProps
      }
    }
  }
}

Rendering

Web Components

The Shadow DOM, which encapsulates and renders content for a specific component, is created and linked to the corresponding component during its instantiation. This operation takes place on the client side.

Consequently, the speedlify-score module must be evaluated only after the complete loading of the site document.

Dynamic Imports

I'm using Vite which utilizes Dynamic Imports. I implemented a component to handle rendering of the Speedlify component.

    *.css    
/* Allocate space for speedlify-score to avoid layout shift */
.score-container {
  height: 43px;
  width: 258px;
}
    *.tsx    
// Solid component
export default function Speedlify() {
  const [data, setData] = createSignal(null)

  onMount(() => {
    async function fetchVitals() {
      if (typeof window !== 'undefined') {
        // async import
        await import('speedlify-score')

        // ...

  })

  return (
    <div class={`score-container ${data() && 'loading-item'}`}>
      {data() && (
        <speedlify-score
          speedlify-url={SPEEDLIFY_URL}
          raw-data={data()}
          hash={SPEEDLIFY_HASH}
          score
          weight
        />
      )}
    </div>
  )

The speedlify-score module will be included in the final bundle, but it will be in a separate chunk that is loaded only when the await import('speedlify-score') statement is executed.

Cache Optimization

I didn't see much need for a more robust caching solution since scores don't need to update often.
Alternatively, you can cache the data on the server and include the performance data in your js bundle.

A high-level schematic representation of the internal workings of the web component highlighting where we can improve rendering performance using the browser's localStorage:

Diagram of Speedlify-Component methods

Here's the rest of the onMount lifecycle function:

    *.tsx    
  onMount(() => {
    async function fetchVitals() {
      if (typeof window !== 'undefined') {
        await import('speedlify-score')
        const url = `${SPEEDLIFY_URL}/api/${SPEEDLIFY_HASH}.json`

        // Try to get the data from localStorage
        const cachedData = localStorage.getItem(url)
        if (cachedData) {
          try {
            const { data, timestamp } = JSON.parse(cachedData)
            setData(data)

            // If the data is less than 24 hours old, don't fetch new data
            if (Date.now() - timestamp < ONE_DAY) {
              return
            }
          } catch (error) {
            console.error('Error parsing data:', error)
          }
        }

        // Fetch new data and store it in localStorage with a timestamp
        try {
          const result = await fetch(url).then((res) => res.json())
          if (result) {
            localStorage.setItem(url, JSON.stringify({ data: result, timestamp: Date.now() }))
            setData(result)
          } else {
            console.error('Fetched data is undefined')
          }
        } catch (error) {
          console.error('Error fetching data:', error)
        }
      }
    }

    fetchVitals()
  })

Here's how it works:

  1. When our component mounts, it calls the fetchVitals() function. This function first checks if the speedlify-score module is available. If it is, it constructs a URL to fetch the performance data.

  2. Before making the network request, it tries to fetch the data from localStorage. If the data is available and less than 24 hours old, it uses this cached data instead of making a new request. This can significantly improve the performance of our component by reducing unnecessary network requests.

  3. If the cached data is not available or more than 24 hours old, it fetches the data from the Speedlify API and stores it in localStorage along with the current timestamp. This data is then used to update the component.

This simple caching strategy improves the performance of the Speedlify component in my Solid app. By reducing unnecessary network requests, we can ensure that our component loads quickly and efficiently, providing a better user experience.

Result

Performance Chart

Here is the performance graph showcasing the initial load time of this implementation without using localStorage cache:

Performance chart of speedlify component

  • Blue Line (DOMContentLoaded): This line indicates when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading. Any scripts that are running when this event fires are typically manipulating the DOM, and they can delay the DOMContentLoaded event.

  • Red Line (load event): This line indicates when the entire page and all its related resources like stylesheets, images, and scripts have been fully loaded.

Observing the Onload Event, we can discern that the page has been completely loaded, and the Speedlify component, being a non-critical element of the page, is rendered subsequently. It's important to note that this scenario is demonstrated with the cache disabled. Therefore, the rendering of the Speedlify component is deferred until the data fetching is completed, as indicated by the script execution timeline.