我的个人博客

如何在NextJS中创建带有模糊加载效果的图片

Closeup photo of white petaled flowers
Published on
/5 mins read/---

NextJS提供了一个内置的图片组件, 它具有许多有用的功能,我们可以利用它们结合一些自定义样式来创建一个带有模糊加载效果的漂亮图片。

NOTE

以下所有示例都是使用React和TypeScript编写的,样式使用Tailwind CSS

模糊图片

这里的简单思路是首先让图片模糊(使用blur-xl类),然后在图片加载完成时通过移除模糊效果(使用blur-0)来淡入显示。

image.tsx
'use client'
 
import { clsx } from 'clsx'
import type { ImageProps as NextImageProps } from 'next/image'
import NextImage from 'next/image'
import { useState } from 'react'
 
export interface ImageProps extends Omit<NextImageProps, 'src' | 'priority'> {
  src: string
}
 
export function Image(props: ImageProps) {
  let { alt, src, loading = 'lazy', style, className, ...rest } = props
  let [loaded, setLoaded] = useState(false)
 
  return (
    <div
      className={clsx(
        // 给父元素添加一个`image-container`类
        // 使其更容易在mdx文件内容中调整样式
        'image-container overflow-hidden',
        !loaded && 'animate-pulse [animation-duration:4s]',
        className
      )}
    >
      <NextImage
        className={clsx(
          '[transition:filter_500ms_cubic-bezier(.4,0,.2,1)]',
          'h-full max-h-full w-full object-center',
          loaded ? 'blur-0' : 'blur-xl'
        )}
        src={src}
        alt={alt}
        style={{ objectFit: 'cover', ...style }}
        loading={loading}
        priority={loading === 'eager'}
        quality={100}
        onLoad={() => setLoaded(true)}
        {...rest}
      />
    </div>
  )
}

我使用Tailwind的blur滤镜工具来创建模糊效果。 你可以通过将blur工具与其他工具如grayscalescale等混合使用来创建自己的变体。 (记得同时更新transition属性)。

调整大小

该组件会根据子图片自动调整大小,你可以传递className来自定义其大小。 例如:

<Image
  src={logo}
  alt={org}
  className="h-12 w-12 shrink-0 rounded-md"
  style={{ objectFit: 'contain' }}
  width={200}
  height={200}
/>

MDX支持

如果你想使用该组件在MDX文件中渲染图片,你需要更新tailwind typography配置 以使图片具有响应式特性。

tailwind.config.js
module.exports = {
  theme: {
    extend: {
      typography: ({ theme }) => ({
        DEFAULT: {
          css: {
            '.image-container': {
              width: 'fit-content',
              marginLeft: 'auto',
              marginRight: 'auto',
              img: {
                marginTop: 0,
                marginBottom: 0,
              },
            },
            // ... 更多排版样式
          },
        },
      }),
    },
  },
}

避免在每次渲染时都模糊

模糊效果会在每次渲染Image组件时发生(即使图片已经加载完成)。 如果你想避免这种情况,你需要手动控制loaded状态:

image.tsx
'use client'
 
import { clsx } from 'clsx'
import type { ImageProps as NextImageProps } from 'next/image'
import NextImage from 'next/image'
import { usePathname } from 'next/navigation'
import { useState } from 'react'
 
let loadedImages: string[] = []
 
// 检测图片是否已经加载,以避免模糊效果
// 基于路由路径名在每次组件渲染时都发生
function useImageLoadedState(src: string) {
  let pathname = usePathname()
  let uniqueImagePath = pathname + '__' + src
  let [loaded, setLoaded] = useState(() => loadedImages.includes(uniqueImagePath))
  return [
    loaded,
    () => {
      if (loaded) return
      loadedImages.push(uniqueImagePath)
      setLoaded(true)
    },
  ] as const
}
 
export interface ImageProps extends Omit<NextImageProps, 'src' | 'priority'> {
  src: string
}
 
export function Image(props: ImageProps) {
  let { alt, src, loading = 'lazy', style, className, ...rest } = props
  let [loaded, onLoad] = useImageLoadedState(src)
 
  return (
    <div
      className={clsx(
        'image-container overflow-hidden',
        !loaded && 'animate-pulse [animation-duration:4s]',
        className
      )}
    >
      <NextImage
        className={clsx(
          '[transition:filter_500ms_cubic-bezier(.4,0,.2,1)]',
          'h-full max-h-full w-full object-center',
          loaded ? 'blur-0' : 'blur-xl'
        )}
        src={src}
        alt={alt}
        style={{ objectFit: 'cover', ...style }}
        loading={loading}
        priority={loading === 'eager'}
        quality={100}
        onLoad={onLoad}
        {...rest}
      />
    </div>
  )
}

现在图片只会在每个页面首次加载时模糊显示。

TIP

如果你想优先加载首屏上方的图片,可以将loading属性设置为eager

我的博客使用这个组件来渲染图片,你可以在网站上浏览并看到这个漂亮的加载效果。

快乐模糊!