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

Published on

This article is also published at it 邦幫忙 2022 iThome Ironman Contest

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/Kamigami55/nextjs-tailwind-contentlayer-blog-starter/compare/day21-custom-link...day22-custom-image


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

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:

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:

// 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:

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

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

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

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 Article

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

The code changes for this article are as follows: https://github.com/Kamigami55/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!