Many front-end frameworks such as Gatsby come with progressive image loading out of the box. In this article, we look at how to achieve this with NextJS, using next/image
and plaiceholder
libraries.
Full code for examples can be found here on Github .
Examples in this article are based mostly on Pages Router, but if you are interested in the App Router version, see this repo instead.
What is progressive image loading? Progressive image loading is a strategy where the website displays a low resolution or placeholder image until the actual image is loaded and displayed. Compared to staring at a blank space, this improves user experience by providing awareness on incoming images.
How it looks like (Photos by shun idota on Unsplash ) - (simulated on a throttled network) Install dependencies sharp
Using Next.js' built-in Image Optimization requires sharp as a dependency.
plaiceholder
When loading remote images, next/image
requires blurDataURL
if using placeholder="blur"
.
The following examples use Typescript & TailwindCSS. If you would like to follow along, grab this starter template which comes with everything you'll need.
Install the following dependencies:
yarn install sharp plaiceholder
Statically imported image files import type { NextPage } from 'next'
import Image from 'next/image'
import imageOne from 'public/image-one.jpeg'
const Home: NextPage = () => {
return (
<div className="relative w-[500px] h-[300px]">
<Image
src={imageOne}
placeholder="blur"
fill
sizes="100vw"
style={{
objectFit: 'cover',
}}
alt="Static import image"
/>
</div>
)
}
export default Home
index.tsx For statically imported images, next/image
will automatically generate the blurDataURL
. Since we are using the fill
prop, wrap the component in a relative positioned <div>
and assign it width and height properties.
Remote images Remote images can be either an absolute external URL, or an internal path. When using an external URL, you must add it to domains in next.config.js
.
module.exports = {
images: {
domains: ['assets.example.com'],
},
}
next.config.js Generate the image placeholder data at build time with getStaticProps
:
import type { NextPage, InferGetStaticPropsType } from 'next'
import Image from 'next/image'
import { getPlaiceholder } from 'plaiceholder'
const Home: NextPage<InferGetStaticPropsType<typeof getStaticProps>> = ({
remoteImageProps,
}) => {
return (
<div className="relative w-[500px] h-[300px]">
<Image
src={remoteImageProps.img.src}
placeholder="blur"
blurDataURL={remoteImageProps.base64}
fill
sizes="100vw"
style={{
objectFit: 'cover',
}}
alt="Remote Image"
/>
</div>
)
}
export const getStaticProps = async () => {
// Remote Image (from external url or relative url like `/my-image.jpg`)
const remoteImageProps = await getPlaiceholder(
'https://source.unsplash.com/78gDPe4WWGE'
)
return {
props: {
remoteImageProps,
},
}
}
export default Home
index.tsx Real world examples How about data fetched from an API? The approach is the similar; process the fetched data and generate placeholder data for the images at build time.
Image gallery import Image from 'next/image'
import { getPlaiceholder } from 'plaiceholder'
import type { InferGetStaticPropsType, NextPage } from 'next'
const GalleryPage: NextPage<InferGetStaticPropsType<typeof getStaticProps>> = ({
imagesProps,
}) => {
return (
<div className="my-3 px-3">
<main className="grid grid-cols-3 gap-3">
{imagesProps.map((imagesProp) => (
<div className="relative h-[300px]" key={imagesProp.img.src}>
<Image
src={imagesProp.img.src}
placeholder="blur"
blurDataURL={imagesProp.base64}
layout="fill"
sizes="100vw"
style={{
objectFit: 'cover',
}}
alt={imagesProp.img.src}
/>
</div>
))}
</main>
</div>
)
}
export const getStaticProps = async () => {
// Fetch images
const raw = await fetch('https://shibe.online/api/shibes?count=10')
const data: string[] = await raw.json()
// Process images
const imagesProps = await Promise.all(
data.map(async (src) => await getPlaiceholder(src))
)
return {
props: {
imagesProps,
},
}
}
export default GalleryPage
gallery/index.tsx Loading an image gallery (Photos by shibe.online API ) - (simulated on a throttled network) Blog posts import ghostClient from 'utils/ghost-client'
import Image from 'next/image'
import { getPlaiceholder } from 'plaiceholder'
import type { GetStaticProps, NextPage } from 'next'
import type { IGetPlaiceholderReturn } from 'plaiceholder'
import type { PostOrPage } from '@tryghost/content-api'
interface PostOrPageImgPlaiceholder extends PostOrPage {
featureImageProps: IGetPlaiceholderReturn | null
}
interface BlogPageProps {
posts: PostOrPageImgPlaiceholder[]
}
const BlogPage: NextPage<BlogPageProps> = ({ posts }) => {
return (
<div className="my-3 px-3">
<main className="grid grid-cols-3 gap-x-3 gap-y-6">
{posts.map((post) => (
<article key={post.id}>
{post.feature_image && post.featureImageProps && (
<div className="relative h-[300px]">
<Image
src={post.featureImageProps.img.src}
placeholder="blur"
blurDataURL={post.featureImageProps.base64}
fill
sizes="100vw"
style={{
objectFit: 'cover',
}}
alt={post.title}
/>
</div>
)}
<h1 className="text-lg font-medium mb-1 mt-3">{post.title}</h1>
</article>
))}
</main>
</div>
)
}
export const getStaticProps: GetStaticProps = async () => {
// Fetch posts
const rawPosts = await ghostClient.posts.browse({
limit: 'all',
})
if (!rawPosts) {
return {
notFound: true,
}
}
// Process images
const posts = await Promise.all(
rawPosts.map(async (post) => {
return {
featureImageProps: post.feature_image
? await getPlaiceholder(post.feature_image)
: null,
...post,
} as PostOrPageImgPlaiceholder
})
)
return {
props: {
posts,
},
}
}
export default BlogPage
blog/index.tsx Loading images from a Ghost blog (simulated on a throttled network) Contentful API - Images in Rich Text Field Here's how to render images from rich text fields in Contentful using next/image
.
After fetching the data, loop through your rich text field to generate the placeholder data, and render the field with documentToReactComponents()
. Create a custom renderer for BLOCKS.EMBEDDED_ASSET
node type using renderOptions
and use the placeholder data.
import contentfulClient from 'utils/contentful-client'
import { documentToReactComponents } from '@contentful/rich-text-react-renderer'
import { BLOCKS } from '@contentful/rich-text-types'
import Image from 'next/image'
import type { InferGetStaticPropsType, NextPage } from 'next'
import type { Options } from '@contentful/rich-text-react-renderer'
import type { Document } from '@contentful/rich-text-types'
import { getPlaiceholder } from 'plaiceholder'
const renderOptions: Options = {
renderNode: {
[BLOCKS.EMBEDDED_ASSET]: (node) => {
const imgBlurData = node.data.target.fields.file.imgBlurData
return (
<div className="relative">
<Image
{...imgBlurData.img}
alt={node.data.target.fields.description}
placeholder="blur"
blurDataURL={imgBlurData.base64}
/>
</div>
)
},
},
}
interface PageFields {
title: string
slug: string
richTextBody: Document
}
const ContentfulPage: NextPage<
InferGetStaticPropsType<typeof getStaticProps>
> = ({ page }) => {
return (
<main className="container mx-auto">
<article>
<h1 className="text-2xl my-10">{page.fields.title}</h1>
<div>
{documentToReactComponents(page.fields.richTextBody, renderOptions)}
</div>
</article>
</main>
)
}
export const getStaticProps = async () => {
const page = await contentfulClient.getEntry<PageFields>(
'53yJ05JcurUI5p9NA1FCrh'
)
// Add blur placeholder data to rich text body for images
for (const node of page.fields.richTextBody.content) {
if (node.nodeType === BLOCKS.EMBEDDED_ASSET) {
node.data.target.fields.file.imgBlurData = await getPlaiceholder(
`https:${node.data.target.fields.file.url}`
)
}
}
return {
props: {
page,
},
}
}
export default ContentfulPage
components/RichTextContent.tsx Using App Router import Image from 'next/image'
import { getPlaiceholder } from 'plaiceholder'
const fetchImages = async () => {
// Fetch images
const raw = await fetch('https://shibe.online/api/shibes?count=10')
const data: string[] = await raw.json()
// Process images
return await Promise.all(data.map(async (src) => await getPlaiceholder(src)))
}
export default async function Gallery() {
const plaiceholders = await fetchImages()
return (
<div className="my-3 px-3">
<h1 className="text-2xl mb-6">Loading from a gallery</h1>
<main className="grid grid-cols-3 gap-3">
{plaiceholders.map((plaiceholder) => (
<div className="relative h-[300px]" key={plaiceholder.img.src}>
<Image
src={plaiceholder.img.src}
placeholder="blur"
blurDataURL={plaiceholder.base64}
alt={plaiceholder.img.src}
fill
sizes="100vw"
style={{
objectFit: 'cover',
}}
/>
</div>
))}
</main>
</div>
)
}
app/gallery/page.tsx