One easy way to improve the speed of a website is to only download images only when they’re needed, which would be when they enter the viewport. This “lazy loading” technique has been around a while and there are lots of great tutorials on how to implement it.
But even with all the resources out there, implementing lazy loading can look different depending on the project you’re working in or the framework you’re using. In this article, I’ll use the Intersection Observer API alongside the onLoad event to lazy load images with the Svelte JavaScript framework.
Check out Tristram Tolliday’s introduction to Svelte if you’re new to the framework.
Let’s work with a real-life example
I put this approach together while testing the speed on a Svelte and Sapper application I work on, Shop Ireland. One of our goals is to make the thing as fast as we possible can. We hit a point where the homepage was taking a performance hit because the browser was downloading a bunch of images that weren’t even on the screen, so naturally, we turned to lazy loading them instead.
Svelte is already pretty darn fast because all of the code is compiled in advance. But once we tossed in lazy loading for images, things really started speeding up.
This is what we’re going to work on together. Feel free to grab the final code for this demo from GitHub and read along for an explanation of how it works.
This is where we’ll end up by the end:
CodePen Embed Fallback
Let’s quickly start up Svelte
You might already have a Svelte app you’d like to use, but if not, let’s start a new Svelte project and work on it locally. From the command line:
npx degit sveltejs/template my-svelte-project
cd my-svelte-project
npm install
npm run dev
You should now have a beginner app running on http://localhost:5000.
Adding the components folder
The initial Svelte demo has an App.svelte file but no components just yet. Let’s set up the components we need for this demo. There is no components folder, so let’s create one in the src folder. Inside that folder, create an Image folder — this will hold our components for this demo.
We’re going to have our components do two things. First, they will check when an image enters the viewport. Then, when an image does enter, the components will wait until the image file has loaded before showing it.
The first component will be an
Let’s start with the
Observing the intersection
Our first component is going to be a working implementation of the Intersection Observer API. The Intersection Observer is a pretty complex thing but the gist of it is that it watches a child element and informs us when it enters the bounding box of its parent. Hence images: they can be children of some parent element and we can get a heads up when they scroll into view.
While it’s definitely a great idea to get acquainted with the ins and outs of the Intersection Observer API — and Travis Almand has an excellent write-up of it — we’re going to make use of a handy Svelte component that Rich Harris put together for svelte.dev.
We’ll set this up first before digging into what exactly it does. Create a new IntersectionObserver.svelte file and drop it into the src/components/Image folder. This is where we’ll define the component with the following code:
?
?
We can use this component as a wrapper around other components, and it will determine for us whether the wrapped component is intersecting with the viewport.
If you’re familiar with the structure of Svelte components, you’ll see it follows a pattern that starts with scripts, goes into styles, then ends with markup. It sets some options that we can pass in, including a once property, along with numeric values for the top, right, bottom and left distances from the edge of the screen that define the point where the intersection begins.
We’ll ignore the distances but instead make use of the once property. This will ensure the images only load once, as they enter the viewport.
The main logic of the component is within the onMount section. This sets up our observer, which is used to check our element to determine if it’s “intersecting” with the visible area of the screen.
For older browsers it also attaches a scroll event to check whether the element is visible as we scroll, and then it’ll remove this listener if we’ve determined that it is viable and that once is true.
Loading the images
Let’s use our
That means we’ll need a new component file in components/Image. Let’s call it ImageLoader.svelte. Here’s the code we want in it:
?
{#if intersecting}
{/if}
This component takes some image-related props — src and alt — that we will use to create the actual markup for an image. Notice that we’re importing two components in the scripts section, including the
The
Then we make use of Svelte’s slot props. What are those? Let’s cover that next.
Slotting property values
Wrapping component, like our
In our
This is passing the intersecting prop into whatever component we give it. In this case, our
We can then use the intersecting value to determine when it’s time to load an
{#if intersecting}
{/if}
If the intersection is happening, the
We now have the code in place to show an
Showing images on load
Yep, you guessed it: let’s add an Image.svelte file to the components/Image folder for our element.
Here’s the component code:
?
?
Right off the bat, we’re receiving the alt and src props before defining two new variables: loaded to store whether the image has loaded or not, and thisImage to store a reference to the img DOM element itself.
We’re also using a helpful Svelte method called onMount. This gives us a way to call functions once a component has been rendered in the DOM. In this case, we’re set a callback for thisImage.onload. In plain English, that means it’s executed when the image has finished loading, and will set the loaded variable to a true value.
We’ll use CSS to reveal the image and fade it into view. Let’s give set an opacity: 0 on images so they are initially invisible, though technically on the page. Then, as they intersect the viewport and the
That leads us to the very last line of the file, which is the markup for an element.
This uses class:loaded to conditionally apply a .loaded class if the loaded variable is true. It also uses the bind:this method to associate this DOM element with the thisImage variable.
Native lazy loading
While support for native lazy loading in browsers is almost here, it’s not yet supported across all the current stable versions. We can still add support for it using a simple capability check.
In our ImageLoader.svelte file we can bring in the onMount function, and within it, check to see if our browser supports lazy loading.
import { onMount } from svelte
let nativeLoading = false
// Determine whether to bypass our intersecting check
onMount(() => {
if (loading in HTMLImageElement.prototype) {
nativeLoading = true
}
})
We then adjust our if condition to include this nativeLoading boolean.
{#if intersecting || nativeLoading}
{/if}
Lastly, in Image.svelte, we tell our browser to use lazy loading by adding loading=lazy to the element.
This lets modern and future browsers bypass our code and take care of the lazy loading natively.
Let’s hook it all up!
Alright, it’s time to actually use our component. Crack open the App.svelte file and drop in the following code to import our component and use it:
?
Here’s the demo once again:
CodePen Embed Fallback
And remember that you’re welcome to download the complete code for this demo on GitHub. If you’d like to see this working on a production site, check out my Shop Ireland project. Lazy loading is used on the homepage, category pages and search pages to help speed things up. I hope you find it useful for your own Svelte projects!