# Optimizing Image Performance Using Next.js Image, plaiceholder, and Custom MDX Components - Modern Next.js Blog Series #22

- Canonical: https://easonchang.com/posts/post-custom-image
- Date: 2022-10-07T00:00:00.000Z
- Language: en
- Description: Optimizing in-text image performance using a customized MDX component CustomImage, in conjunction with Next.js <Image/> and the plaiceholder package

> This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10305758)

Website performance bottlenecks are often due to slow image loading speeds. To enhance reader experience and SEO scores, this article will focus on optimizing in-text image performance!

> The code changes for this article are as follows:
> https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day21-custom-link...day22-custom-image

---

## Adding a Customized `<CustomImage/>` to Optimize Image Loading Speed

### Installing Related Packages

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

### Allowing Next.js Image to Use webp, avif Formats

Modify `next.config.mjs` by adding an images section:

```js
import { withContentlayer } from "next-contentlayer";

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

export default nextConfig;
```

### Using Custom imageMetadata rehype Plugin to Add Length and Width Attributes and LQIP to Images

Add `src/plugins/imageMetadata.ts`:

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

// Similar 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 its 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;
  };
}
```

Modify `contentlayer.config.ts` to apply the above-written imageMetadata rehype plugin:

```ts
import imageMetadata from "./src/plugins/imageMetadata";

// ...

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

Add `src/components/CustomImage.tsx`:

```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}
    />
  );
}
```

Modify `src/lib/mdxComponents.ts` so that all imgs in MDX use CustomImage for rendering:

```ts
import CustomImage from "@/components/CustomImage";
// ...

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

export default mdxComponents;
```

## Results

Done! Use `pnpm dev`, and enter any article with images to see the loading speed has improved!

A blurred version of the image will also be displayed while loading, informing readers that an image will appear there, also preventing layout shifts.

## References

- [next/image | Next.js](https://nextjs.org/docs/api-reference/next/image#acceptable-formats)
- [Using the Next.js Image Component with MDX](https://kylepfromer.com/blog/nextjs-image-component-blog/)

## Next Article

Congratulations on customizing in-text images and speeding up loading!

> The code changes for this article are as follows:
> https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day21-custom-link...day22-custom-image

In the next article, we will use another method to optimize the perceived page transition speed for readers, adding a page transition progress bar with nprogress!
