I recently decided to lazy load images on a Sapper powered website to optimize the initial loading time. I did this by using a placeholder URL and as soon as the image is visible to the user, replacing this placeholder URL with the real deal. Similar to the gatsby-image-approach (obviously not as sophisticated). Let me take you through the process :)
1. Detect when the image is visible
The first step is to make sure we can detect when an element (in our case an img
) is first visible to the user. This can be achieved with the Intersection Observer API. This API is really useful to detect intersections between an element and its ancestor or in our case the top-level viewport.
To prevent instantiating a new IntersectionObserver
for each image we will write an intersection service. To do that, let's define a variable in which the observer
will be saved and a Map
which we will use to keep track of all the elements in the observer:
let observer
const elements = new Map()
Next we make sure this same IntersectionObserver
is always used:
const getObserver = () => {
if (!observer) {
observer = initObserver()
}
return observer
}
The initObserver
function is referenced in the previous snippet, but not yet implemented, let's do that:
const initObserver = () => {
return new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const lazy = entry.target
observer.unobserve(lazy)
if (elements.has(lazy)) {
elements.get(lazy)()
elements.delete(lazy)
}
}
})
})
}
The new observer watches intersections with each of it's targets which we will add soon. As soon as an intersection is detected (entry.isIntersecting
) we don't need to observe the element anymore, since the real image URL is loaded. Then if we find the element in our map tracking the elements (which we normally should) we call the function saved in the map and delete the entry, since we don't use it anymore.
To observe an element we use the only function exported from this service: observe
:
export const observe = (element) => {
const obs = getObserver()
return new Promise((resolve) => {
elements.set(element, resolve)
obs.observe(element)
})
}
The observe
function returns a promise which is resolved as soon as the element is intersecting with the viewport (is visible).
2. Svelte Component
The next step is to implement a svelte component using this service to replace a placeholder URL with the real source. This is fairly simple:
<script>
import { onMount } from 'svelte'
import { observe } from './intersection.service'
let image, source
onMount(async () => {
source = placeholder
await observe(image)
source = src
})
export let src, placeholder, alt
</script>
<img src={source} {alt} bind:this={image} {...$$restProps} />
On mounting we set the image source to the placeholder and as soon as the image is visible we swap the sources. The ...$$restProps
is there to make sure, things like styles and other attributes get applied to the img
.
3. Use it
The final usage is pretty simple:
<Image
alt="clouds"
style="width: 100%"
src="https://images.unsplash.com/photo-1587476821668-7e1391103e49?w=1600"
placeholder="https://images.unsplash.com/photo-1587476821668-7e1391103e49?w=16" />
4. Next steps
To make sure this can be used in all browsers you are supporting you might need to add an intersection observer polyfill
Furthermore we could optimize this approach by automatically compress images during bundling and using them as the placeholders.
Top comments (0)