Blurred image placeholder for Next.js image (next/image)
As seen in gif above (a bit distorted) is what I have accomplished described in the post below. And on this image below you can see the blurred image which is a base64-encoded image loaded first and it's created from the image to the right with a webpack-loader.
UPDATE
It was initial written about lqip-loader
but after some npm vulnerabilities and lack of support
and decided to rewrite it and luckily found lqip-modern-loader
. So updated content follows
When I started using Next.js for the first time for real were just in break between version 9 and 10. So when I heard they should include a new optimized image component I were excited.
It's nice when the framework adds things like this which is build together with framework and optimized for it. But as in many cases it could missing features you thought would be nice to have. One of those things are some sort of placeholder for the image before it's loads because of the lazy loading.
I have experiment with a lot of other npm packages such as next-optimized-images
and got to know
lqip for the first time. So why didn't I went that way? It have it's pros and cons as next/image
and I want to stick to it and see how it evolves and in my opinion feels a bit more future-proof.
The solution
So the solution were that it implemented a webpack loader which gives me a base64 string of the
image I can add into a wrapper together with next/image
which is absolute positioned and when the
image loads it will appear above the placeholder and you will get the illusion they are stitched
together.
npm install lqip-modern-loader --save-dev
Add the lqip-modern-loader
into the next.config.js and it will provided you with the base64
image which we uses in our custom image component.
module.exports = {
webpack: (config, options) => {
config.module.rules.push({
test: /\.(gif|png|webp|jpe?g)$/i,
use: [
{
loader: 'lqip-modern-loader'
}
]
})
return config
}
}
We still using the Next.js pattern to store our images into the public
-directory and when we're
doing the required we are loading it through our newly added webpack loader.
import Image, {ImageProps} from 'next/image'
const CustomImage = ({src, ...props}: ImageProps) => {
const image = require(`../public${src}?lqip`)
return (
<div className="relative overflow-hidden flex rounded-none sm:rounded-md">
<img
src={image.dataURI}
className="absolute inset-0 w-full h-full transform scale-110 m-0"
style={{filter: 'blur(20px)'}}
aria-hidden="true"
alt=""
/>
<Image src={image.src} {...props} />
</div>
)
}
export {CustomImage}
Here you can see an example of what you will get back from
const image = src.startsWith('http') ? {src} : require(`../public${src}?lqip`)
// const image = src.startsWith('http') ? {src} : require(`../public${src}?lqip`)
// console.log(image)
{
"src": "/_next/static/media/home-office.0484ab3be39e7d374d77eea012746d12.jpg",
"width": 24,
"height": 18,
"dataURI": "data:image/jpg;base64,/9j/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAASABgDASIAAhEBAxEB/8QAGQABAAMBAQAAAAAAAAAAAAAAAAgJCgYH/8QAJxAAAQUBAAIBBAIDAQAAAAAAAwECBAUGBwAIEQkSExQVMRYYJTL/xAAXAQADAQAAAAAAAAAAAAAAAAAEBQYH/8QAKhEAAwABAgMFCQAAAAAAAAAAAQIDBAARBRITBjNyc7EUISIkQkNRssH/2gAMAwEAAhEDEQA/AKW+C8s93vaXq1bG41yN3U+fR+aUWrk0+FrGzdcDPTayPGj6C3A+UaWCazQMLCd9z4tUaKUZFEE5EYOfvoP6wV3uh2bQcdW+jc9uMzSZ+xLZmgzOgfmtNDro2PjVEulyDm2FHEhWMlXW2lsyJS1j2Miy3Cecb0pbxXsp3TgPNuS2fP8AoNvy+p2VJL5tYbet1Gtzv8a11lMlfrapmOmRbK4yTTxLpo4oRq6vPUDmi/JIGiOt14T9Q33d59iq6FyfC/T56rZc4y9JHl6HDy8yzWWOOkn/AIxpt5+pFo93oDXKoqFsrXWOkyLxG2kxHTWCRFVMvtA2RxJevhyxGMhwm8S/tUSJwGS2clyYUWeQLiYgyfL0jzIKo70qzxUoMWmNl0LBnrlxtFaBwygzlj1Ioy/lVMQikMDR9yFlP7Heq3+s3Uthyo95B0snGWI6mRe10WTBhTyvgQpz3giSyFOBA/uNC5rykVSic5r1arV8ecz3b2Z2nZ87Xb7rfJMpwTY02Tubfd0HNlu7L/qo+wSsnzbnQ3F5HuySWxoLXPAR6RRyVZIG9RoTx5V4HHFxcLEnnE2yhGYtWfS5K1VVWlAA4ADuCwA93KQBsNWsO02Hi4+KuVuavCVGZOlsSQoYlUcBGLKxMyAyH4SoI21lq7Gc7uE44LjFcEN9JUInEeoxKtvrCKomK77RqrzFev2ony8pHf29yr719MBV/wA07eT5X8ichhNR/wAr96Nd0XGI5Ed/6RHIiIqfPwvwnz/SePHiQ9zl+bT0TWTL30fBP1bWiDusmTJveoVsiQc9cfC7gR4BikLCMOMHmbo4yxXucAjI7pUlwWPGrRLIOo0apifc8ePEzfT4R/dGW+z5S/vr/9k="
}
And an example of the CustomImage component in use. Still needing every necessary properties as
next/image
<CustomImage
src="/static/images/home-office.jpeg"
alt="My computer desk at home"
width="4032"
height="3024"
/>
Caveats
One problem with this solution are that it's only supports jpg and png at the moment. And to
have fallback solutions is to add another webpack loader file-loader
.
npm install file-loader
We then update next.config.js
with the following code
module.exports = {
webpack: (config, options) => {
config.module.rules.push({
test: /\.(gif|png|webp|jpe?g)$/i,
use: [
{
loader: 'lqip-modern-loader'
}
]
})
config.module.rules.push({
test: /\.(svg|png|jpe?g|gif|webp|mp4)$/i,
use: [
{
loader: 'file-loader',
options: {
publicPath: '/_next',
name: 'static/media/[name].[hash].[ext]'
}
}
]
})
return config
}
}
Update the CustomImage
-component to support fallback source for not supported image types.
import Image, {ImageProps} from 'next/image'
export type CustomImageType = ImageProps & {
noPlaceholder?: boolean
}
const CustomImage = ({src, noPlaceholder, ...props}: CustomImageType) => {
const image = src.startsWith('http') ? {src} : require(`../public${src}?lqip`)
return (
<div className="relative overflow-hidden flex rounded-none sm:rounded-md">
{image.dataURI && !noPlaceholder && (
<img
src={image.dataURI}
className="absolute inset-0 w-full h-full transform scale-110 m-0"
style={{filter: 'blur(20px)'}}
aria-hidden="true"
alt=""
/>
)}
<Image src={image?.src || image.default} {...props} />
</div>
)
}
export {CustomImage}
As you seen above I have added support for external images and also added the property
noPlaceholder
if you have transparent images and it be a bit weird when you get a blurred image
behind it. So with new prop you have opt-out the placeholder.
Hope you find this useful and if you have any questions or improvements suggestion just reach out to me on Twitter @jarnesjo