iT邦幫忙

2022 iThome 鐵人賽

DAY 22
0
Modern Web

從零開始打造炫砲個人部落格,使用 Next.js、ContentLayer、i18next 等現代技術系列 第 22

圖片效能最佳化,使用 Next.js Image、plaiceholder、客製 MDX 元件 - Modern Next.js Blog 系列 #22

  • 分享至 

  • xImage
  •  

網站效能瓶頸通常是圖片讀取速度太慢。為了提升讀者體驗、和 SEO 分數,這一篇我們來最佳化內文圖片效能!

這篇修改的程式碼如下:
https://github.com/Kamigami55/nextjs-tailwind-contentlayer-blog-starter/compare/day21-custom-link...day22-custom-image

我的個人網站裡也有此系列的好讀版,程式碼更易讀、也支援深色模式和側邊目錄,歡迎前往閱讀!


加入客製化 <CustomImage/>,最佳化圖片讀取速度

安裝相關套件

pnpm add image-size plaiceholder sharp unist-util-visit

允許 Next.js Image 使用 webp, avif 格式

修改 next.config.mjs,加入 images 區塊:

import { withContentlayer } from 'next-contentlayer';

/** @type {import('next').NextConfig} */
const nextConfig = withContentlayer({
  // ...
  // 加入 images 區塊
  images: {
    // Enable modern image formats
    formats: ['image/avif', 'image/webp'],
  },
});

export default nextConfig;

使用客製 imageMetadata rehype plugin,幫圖片加入長寬屬性和 LQIP

新增 src/plugins/imageMetadata.ts

// Custom rehype plugin to add width and height to local images
// To make Next.js <Image/> works
// Ref: https://kylepfromer.com/blog/nextjs-image-component-blog

// Similiar structure to:
// https://github.com/JS-DevTools/rehype-inline-svg/blob/master/src/inline-svg.ts
import imageSize from 'image-size';
import path from 'path';
import { getPlaiceholder } from 'plaiceholder';
import { Node, visit } from 'unist-util-visit';
import { promisify } from 'util';

const sizeOf = promisify(imageSize);

/**
 * An `<img>` HAST node
 */
interface ImageNode extends Node {
  type: 'element';
  tagName: 'img';
  properties: {
    src: string;
    height?: number;
    width?: number;
    base64?: string;
  };
}

/**
 * Determines whether the given HAST node is an `<img>` element.
 */
function isImageNode(node: Node): node is ImageNode {
  const img = node as ImageNode;
  return (
    img.type === 'element' &&
    img.tagName === 'img' &&
    img.properties &&
    typeof img.properties.src === 'string'
  );
}

/**
 * Filters out non absolute paths from the public folder.
 */
function filterImageNode(node: ImageNode): boolean {
  return node.properties.src.startsWith('/');
}

/**
 * Adds the image's `height` and `width` to it's properties.
 */
async function addMetadata(node: ImageNode): Promise<void> {
  const res = await sizeOf(
    path.join(process.cwd(), 'public', node.properties.src)
  );

  if (!res) throw Error(`Invalid image with src "${node.properties.src}"`);
  const { base64 } = await getPlaiceholder(node.properties.src, { size: 10 }); // 10 is to increase detail (default is 4)

  node.properties.width = res.width;
  node.properties.height = res.height;
  node.properties.base64 = base64;
}

/**
 * This is a Rehype plugin that finds image `<img>` elements and adds the height and width to the properties.
 * Read more about Next.js image: https://nextjs.org/docs/api-reference/next/image#layout
 */
export default function imageMetadata() {
  return async function transformer(tree: Node): Promise<Node> {
    const imgNodes: ImageNode[] = [];

    visit(tree, 'element', (node) => {
      if (isImageNode(node) && filterImageNode(node)) {
        imgNodes.push(node);
      }
    });

    for (const node of imgNodes) {
      await addMetadata(node);
    }

    return tree;
  };
}

修改 contentlayer.config.ts,套用上面寫的 imageMetadata rehype plugin:

import imageMetadata from './src/plugins/imageMetadata';

// ...

export default makeSource({
  // ...
  mdx: {
    rehypePlugins: [
      // ...
      imageMetadata, // For adding image metadata (width, height)
    ],
  },
});

新增 src/components/CustomImage.tsx

import Image, { ImageProps } from 'next/image';

type Props = ImageProps & { base64?: string };

export default function CustomImage({
  src,
  height,
  width,
  base64,
  alt,
  ...otherProps
}: Props) {
  if (!src) return null;
  if (typeof src === 'string' && (!height || !width)) {
    return (
      // eslint-disable-next-line @next/next/no-img-element
      <img src={src} height={height} width={width} alt={alt} {...otherProps} />
    );
  }
  return (
    <Image
      layout="responsive"
      src={src}
      alt={alt}
      height={height}
      width={width}
      sizes="(min-width: 40em) 40em, 100vw"
      placeholder={base64 ? 'blur' : 'empty'}
      blurDataURL={base64}
      {...otherProps}
    />
  );
}

修改 src/lib/mdxComponents.ts,讓 MDX 裡面的 img 都使用 CustomImage 來渲染:

import CustomImage from '@/components/CustomImage';
// ...

// Custom components/renderers to pass to MDX.
const mdxComponents = {
  // ...
  img: CustomImage,
};

export default mdxComponents;

成果

完成了!使用 pnpm dev,進到任何一篇有圖片的文章,就會看到讀取速度變快了!

在讀取時也會先顯示模糊版本的圖片,讓讀者知道那邊將有圖片,也避免版面位移。

References

下一篇

恭喜你成功客製化了內文圖片,加快了讀取速度!

這篇修改的程式碼如下:
https://github.com/Kamigami55/nextjs-tailwind-contentlayer-blog-starter/compare/day21-custom-link...day22-custom-image

下一篇我們會來用另一個手段,最佳化讀者的體感換頁速度,使用 nprogress 加入換頁進度條!


上一篇
強化內文連結換頁速度、加入外部連結 icon - Modern Next.js Blog 系列 #21
下一篇
使用 nprogress 加入換頁進度條 - Modern Next.js Blog 系列 #23
系列文
從零開始打造炫砲個人部落格,使用 Next.js、ContentLayer、i18next 等現代技術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言